diff --git a/package-lock.json b/package-lock.json index e81dcb9dd6c..999529b5621 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,15 +23,19 @@ "n3": "^1.23.1", "object-hash": "^3.0.0", "object-sizeof": "^2.6.5", + "piscina": "^5.1.4", "rotating-file-stream": "^3.2.6", "seedrandom": "^3.0.5", "semver": "^7.7.1", + "serialize-javascript": "^7.0.2", "tar": "^7.4.3", + "tinypool": "^2.0.0", "tmp": "^0.2.3", "ts-essentials": "^10.1.1", "tslog": "^4.9.3", "web-tree-sitter": "^0.24.7", "ws": "^8.18.0", + "wtfnode": "^0.10.1", "xpath-ts2": "^1.4.2" }, "devDependencies": { @@ -49,8 +53,10 @@ "@types/object-hash": "^3.0.6", "@types/seedrandom": "^3.0.8", "@types/semver": "^7.7.0", + "@types/serialize-javascript": "^5.0.4", "@types/tmp": "^0.2.6", "@types/ws": "^8.18.1", + "@types/wtfnode": "^0.10.0", "@typescript-eslint/eslint-plugin": "^8.40.0", "@vitest/coverage-v8": "^3.2.4", "esbuild": "^0.25.9", @@ -1340,18 +1346,28 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.6.tgz", - "integrity": "sha512-62u896rWCtKKE43soodq5e/QcRsA22I+7/4Ov7LESWnKRO6BVo2A1DFLDmXL9e28TB0CfHc3YtkbPm7iwajqkg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1366,14 +1382,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.10.tgz", - "integrity": "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==", + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1388,20 +1404,20 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1416,15 +1432,15 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz", - "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==", + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/external-editor": "^1.0.1", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1439,15 +1455,15 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.13.tgz", - "integrity": "sha512-HgYNWuZLHX6q5y4hqKhwyytqAghmx35xikOGY3TcgNiElqXGPas24+UzNPOwGUZa5Dn32y25xJqVeUcGlTv+QQ==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1462,14 +1478,14 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", - "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.6.3" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { "node": ">=18" @@ -1484,9 +1500,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -1494,14 +1510,14 @@ } }, "node_modules/@inquirer/input": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.10.tgz", - "integrity": "sha512-kV3BVne3wJ+j6reYQUZi/UN9NZGZLxgc/tfyjeK3mrx1QI7RXPxGp21IUTv+iVHcbP4ytZALF8vCHoxyNSC6qg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1516,14 +1532,14 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.13.tgz", - "integrity": "sha512-IrLezcg/GWKS8zpKDvnJ/YTflNJdG0qSFlUM/zNFsdi4UKW/CO+gaJpbMgQ20Q58vNKDJbEzC6IebdkprwL6ew==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1538,15 +1554,15 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.13.tgz", - "integrity": "sha512-NN0S/SmdhakqOTJhDwOpeBEEr8VdcYsjmZHDb0rblSh2FcbXQOr+2IApP7JG4WE3sxIdKytDn4ed3XYwtHxmJQ==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -1561,22 +1577,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.1.tgz", - "integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.1.6", - "@inquirer/confirm": "^5.1.10", - "@inquirer/editor": "^4.2.11", - "@inquirer/expand": "^4.0.13", - "@inquirer/input": "^4.1.10", - "@inquirer/number": "^3.0.13", - "@inquirer/password": "^4.0.13", - "@inquirer/rawlist": "^4.1.1", - "@inquirer/search": "^3.0.13", - "@inquirer/select": "^4.2.1" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { "node": ">=18" @@ -1591,15 +1607,15 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.1.tgz", - "integrity": "sha512-VBUC0jPN2oaOq8+krwpo/mf3n/UryDUkKog3zi+oIi8/e5hykvdntgHUB9nhDM78RubiyR1ldIOfm5ue+2DeaQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1614,16 +1630,16 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.13.tgz", - "integrity": "sha512-9g89d2c5Izok/Gw/U7KPC3f9kfe5rA1AJ24xxNZG0st+vWekSk7tB9oE+dJv5JXd0ZSijomvW0KPMoBd8qbN4g==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1638,17 +1654,17 @@ } }, "node_modules/@inquirer/select": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.1.tgz", - "integrity": "sha512-gt1Kd5XZm+/ddemcT3m23IP8aD8rC9drRckWoP/1f7OL46Yy2FGi8DSmNjEjQKtPl6SV96Kmjbl6p713KXJ/Jg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.11", - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -1663,9 +1679,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -1684,6 +1700,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1701,6 +1718,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1713,12 +1731,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1736,6 +1756,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -1898,6 +1919,311 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1947,209 +2273,175 @@ } }, "node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/request": "^9.2.3", - "@octokit/types": "^14.0.0", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", - "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^13.10.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { "@octokit/core": ">=6" } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^24.2.0" - } - }, "node_modules/@octokit/plugin-request-log": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", - "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^13.10.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { "@octokit/core": ">=6" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^24.2.0" - } - }, "node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", + "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/rest": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", - "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/core": "^6.1.4", - "@octokit/plugin-paginate-rest": "^11.4.2", - "@octokit/plugin-request-log": "^5.3.1", - "@octokit/plugin-rest-endpoint-methods": "^13.3.0" + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", "dev": true, "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@phun-ky/typeof": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-1.2.8.tgz", - "integrity": "sha512-7J6ca1tK0duM2BgVB+CuFMh3idlIVASOP2QvOCbNWDc6JnvjtKa9nufPoJQQ4xrwBonwgT1TIhRRcEtzdVgWsA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-2.0.3.tgz", + "integrity": "sha512-oeQJs1aa8Ghke8JIK9yuq/+KjMiaYeDZ38jx7MhkXncXlUKjqQ3wEm2X3qCKyjo+ZZofZj+WsEEiqkTtRuE2xQ==", "dev": true, "license": "MIT", "engines": { @@ -2164,6 +2456,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2793,6 +3086,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/serialize-javascript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/serialize-javascript/-/serialize-javascript-5.0.4.tgz", + "integrity": "sha512-Z2R7UKFuNWCP8eoa2o9e5rkD3hmWxx/1L0CYz0k2BZzGh0PhEVMp9kfGiqEml/0IglwNERXZ2hwNzIrSz/KHTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -2831,6 +3131,13 @@ "@types/node": "*" } }, + "node_modules/@types/wtfnode": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@types/wtfnode/-/wtfnode-0.10.0.tgz", + "integrity": "sha512-ItEhqP60vy3o3cXYapNHOPNyKE0SiDteX0hf3LdJbGbH0F7Il50G4N7Zr+VLiBWvh8hSR2e6EZ8xRHcO2zqoLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", @@ -3304,26 +3611,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3635,6 +3927,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -3668,9 +3961,9 @@ } }, "node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "dev": true, "license": "Apache-2.0" }, @@ -3701,6 +3994,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3767,27 +4061,27 @@ } }, "node_modules/c12": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.3.tgz", - "integrity": "sha512-uC3MacKBb0Z15o5QWCHvHWj5Zv34pGQj9P+iXKSpTuSGFS0KKhUWf4t9AJ+gWjYOdmWCPEGpEzm8sS0iqbpo1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", - "dotenv": "^16.4.7", - "exsolve": "^1.0.4", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", "giget": "^2.0.0", - "jiti": "^2.4.2", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.1.0", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -3883,9 +4177,9 @@ } }, "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -3960,9 +4254,9 @@ "license": "MIT" }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -3977,16 +4271,16 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -4002,9 +4296,9 @@ } }, "node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -4044,13 +4338,13 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4144,13 +4438,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4608,9 +4895,9 @@ "license": "MIT" }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", "dev": true, "license": "MIT", "dependencies": { @@ -4625,9 +4912,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -4727,9 +5014,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -4765,9 +5052,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4806,12 +5093,13 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, @@ -5403,20 +5691,6 @@ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", @@ -5826,16 +6100,16 @@ } }, "node_modules/eta": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", - "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.5.0.tgz", + "integrity": "sha512-qifAYjuW5AM1eEEIsFnOwB+TGqu6ynU3OKj9WbUTOtUBHFPZqL03XUW34kbp3zm19Ald+U8dEyRXaVsUck+Y1g==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=20" }, "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" + "url": "https://github.com/bgub/eta?sponsor=1" } }, "node_modules/event-target-shim": { @@ -5890,9 +6164,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, "license": "MIT" }, @@ -5909,9 +6183,9 @@ } }, "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", "dev": true, "funding": [ { @@ -6123,6 +6397,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -6224,9 +6499,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, "license": "MIT", "engines": { @@ -6729,9 +7004,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -6739,6 +7014,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -6849,18 +7128,18 @@ } }, "node_modules/inquirer": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.6.0.tgz", - "integrity": "sha512-3zmmccQd/8o65nPOZJZ+2wqt76Ghw3+LaMrmc6JE/IzcvQhJ1st+QLCOo/iLS85/tILU0myG31a2TAZX0ysAvg==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/prompts": "^7.5.0", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", "mute-stream": "^2.0.0", - "run-async": "^3.0.0", + "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "engines": { @@ -7113,6 +7392,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7542,6 +7822,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7554,9 +7835,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { @@ -7903,9 +8184,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -7929,14 +8210,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -8015,31 +8288,18 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8079,9 +8339,9 @@ } }, "node_modules/macos-release": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.3.0.tgz", - "integrity": "sha512-tPJQ1HeyiU2vRruNGhZ+VleWuMQRro8iFtJxYgnS4NQe+EukKF6aGiIT+7flZhISAt2iaXBCfFGvAyif7/f8nQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz", + "integrity": "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==", "dev": true, "license": "MIT", "engines": { @@ -8247,16 +8507,20 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -8288,6 +8552,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8318,13 +8583,12 @@ } }, "node_modules/minizlib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", - "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" + "minipass": "^7.1.2" }, "engines": { "node": ">= 18" @@ -8465,9 +8729,9 @@ } }, "node_modules/node-fetch-native": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", - "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "dev": true, "license": "MIT" }, @@ -8735,23 +8999,38 @@ } }, "node_modules/nypm": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", - "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", + "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.0.0", - "tinyexec": "^0.3.2" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", + "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nypm/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/object-deep-merge": { @@ -8913,16 +9192,16 @@ } }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -8950,29 +9229,45 @@ } }, "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", + "chalk": "^5.6.2", "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", + "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -8984,14 +9279,14 @@ } }, "node_modules/os-name": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.0.0.tgz", - "integrity": "sha512-bv608E0UX86atYi2GMGjDe0vF/X1TJjemNS8oEW6z22YW1Rc3QykSYoGfkQbX0zZX9H0ZB6CQP/3GTf1I5hURg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz", + "integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==", "dev": true, "license": "MIT", "dependencies": { - "macos-release": "^3.2.0", - "windows-release": "^6.0.0" + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" }, "engines": { "node": ">=18" @@ -9110,6 +9405,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -9227,6 +9523,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -9243,6 +9540,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/path-type": { @@ -9276,9 +9574,9 @@ } }, "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "dev": true, "license": "MIT" }, @@ -9325,15 +9623,27 @@ "node": ">=4" } }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, "node_modules/pkg-types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", - "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.2.1", - "exsolve": "^1.0.1", + "confbox": "^0.2.2", + "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, @@ -9615,13 +9925,13 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -9680,9 +9990,9 @@ } }, "node_modules/release-it": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.0.2.tgz", - "integrity": "sha512-tGRCcKeXNOMrK9Qe+ZIgQiMlQgjV8PLxZjTq1XGlCk5u1qPgx+Pps0i8HIt667FDt0wLjFtvn5o9ItpitKnVUA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-19.2.4.tgz", + "integrity": "sha512-BwaJwQYUIIAKuDYvpqQTSoy0U7zIy6cHyEjih/aNaFICphGahia4cjDANuFXb7gVZ51hIK9W0io6fjNQWXqICg==", "dev": true, "funding": [ { @@ -9697,27 +10007,25 @@ "license": "MIT", "dependencies": { "@nodeutils/defaults-deep": "1.1.0", - "@octokit/rest": "21.1.1", - "@phun-ky/typeof": "1.2.8", + "@octokit/rest": "22.0.1", + "@phun-ky/typeof": "2.0.3", "async-retry": "1.3.3", - "c12": "3.0.3", - "ci-info": "^4.2.0", - "eta": "3.5.0", + "c12": "3.3.3", + "ci-info": "^4.3.1", + "eta": "4.5.0", "git-url-parse": "16.1.0", - "inquirer": "12.6.0", + "inquirer": "12.11.1", "issue-parser": "7.0.1", - "lodash.get": "4.4.2", "lodash.merge": "4.6.2", - "mime-types": "3.0.1", + "mime-types": "3.0.2", "new-github-release-url": "2.0.0", - "open": "10.1.2", - "ora": "8.2.0", - "os-name": "6.0.0", + "open": "10.2.0", + "ora": "9.0.0", + "os-name": "6.1.0", "proxy-agent": "6.5.0", - "semver": "7.7.1", - "tinyexec": "1.0.1", - "tinyglobby": "0.2.13", - "undici": "6.21.2", + "semver": "7.7.3", + "tinyglobby": "0.2.15", + "undici": "6.23.0", "url-join": "5.0.0", "wildcard-match": "5.1.4", "yargs-parser": "21.1.1" @@ -9729,13 +10037,6 @@ "node": "^20.12.0 || >=22.0.0" } }, - "node_modules/release-it/node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9866,41 +10167,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.53.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", @@ -9956,9 +10222,9 @@ } }, "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -9969,9 +10235,9 @@ } }, "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", "dev": true, "license": "MIT", "engines": { @@ -10114,9 +10380,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10125,6 +10391,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.2.tgz", + "integrity": "sha512-Y/agDAqbUWRYFWLF5pMT9Rb8wN5ERPMbQH7oq6R+4YgFdMNO4+ELo4PjFCW3H+2CbXirPr/XUgYT+3iXJfTbZQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10506,18 +10781,17 @@ } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", + "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10528,6 +10802,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10542,21 +10817,17 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10647,6 +10918,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10663,6 +10935,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10675,6 +10948,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10806,37 +11080,21 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { "node": ">=18" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -10959,14 +11217,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -10976,13 +11234,12 @@ } }, "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.0.0.tgz", + "integrity": "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==", "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || >=22.0.0" } }, "node_modules/tinyrainbow": { @@ -11275,9 +11532,9 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11353,19 +11610,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -11557,9 +11801,9 @@ } }, "node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", "engines": { @@ -11753,23 +11997,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", @@ -11843,21 +12070,14 @@ } } }, - "node_modules/vitest/node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/vitest/node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": "^18.0.0 || >=20.0.0" } }, "node_modules/web-tree-sitter": { @@ -11994,9 +12214,9 @@ "license": "ISC" }, "node_modules/windows-release": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.0.1.tgz", - "integrity": "sha512-MS3BzG8QK33dAyqwxfYJCJ03arkwKaddUOvvnnlFdXLudflsQF6I8yAxrLBeQk4yO8wjdH/+ax0YzxJEDrOftg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", "dev": true, "license": "MIT", "dependencies": { @@ -12048,6 +12268,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12065,6 +12286,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12074,6 +12296,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -12089,6 +12312,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -12101,18 +12325,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12127,6 +12347,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12181,13 +12402,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -12244,6 +12458,34 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wtfnode": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.10.1.tgz", + "integrity": "sha512-4mcHdlvcdSytsbFueN6QYZxmh5K7REawBk//ZOrJrtVOe548Qsq4GNEm/OUfZATqDnsX4g8uBbzmN7NdEZx09Q==", + "license": "ISC", + "bin": { + "wtfnode": "proxy.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/xpath-ts2": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/xpath-ts2/-/xpath-ts2-1.4.2.tgz", @@ -12341,13 +12583,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -12399,10 +12634,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { + "node_modules/yoctocolors": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 94d8381c0fd..ee0d422cc68 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "wiki:watch": "ts-node-dev src/cli/wiki.ts -- --keep-alive", "build": "tsc --project .", "build-dev": "npm run build && npm run build:copy-wasm", - "build:bundle-flowr": "npm run build && esbuild --bundle dist/src/cli/flowr.js --platform=node --tree-shaking=true --bundle --minify --external:clipboardy --target=node22 --outfile=dist/src/cli/flowr.min.js && npm run build:copy-wasm", + "build:bundle-flowr": "npm run build && esbuild --bundle dist/src/cli/flowr.js --platform=node --tree-shaking=true --bundle --minify --external:clipboardy --target=node22 --outfile=dist/src/cli/flowr.min.js && npm run build:bundle-worker && npm run build:copy-wasm", + "build:bundle-worker": "npm run build && esbuild --bundle dist/src/dataflow/parallel/worker.js --platform=node --tree-shaking=true --bundle --minify --external:clipboardy --target=node22 --outfile=dist/src/cli/worker.js", "build:copy-wasm": "mkdir -p dist/node_modules/@eagleoutice/tree-sitter-r/ && mkdir -p dist/node_modules/web-tree-sitter && cp node_modules/@eagleoutice/tree-sitter-r/tree-sitter-r.wasm dist/node_modules/@eagleoutice/tree-sitter-r/ && cp node_modules/web-tree-sitter/tree-sitter.wasm dist/node_modules/web-tree-sitter/", "lint-local": "npx eslint --version && npx eslint src/ test/ --rule \"no-warning-comments: off\"", "lint": "npm run license-compat -- --summary && npx eslint --version && npx eslint src/ test/", @@ -177,8 +178,10 @@ "@types/object-hash": "^3.0.6", "@types/seedrandom": "^3.0.8", "@types/semver": "^7.7.0", + "@types/serialize-javascript": "^5.0.4", "@types/tmp": "^0.2.6", "@types/ws": "^8.18.1", + "@types/wtfnode": "^0.10.0", "@typescript-eslint/eslint-plugin": "^8.40.0", "@vitest/coverage-v8": "^3.2.4", "esbuild": "^0.25.9", @@ -210,15 +213,19 @@ "n3": "^1.23.1", "object-hash": "^3.0.0", "object-sizeof": "^2.6.5", + "piscina": "^5.1.4", "rotating-file-stream": "^3.2.6", "seedrandom": "^3.0.5", "semver": "^7.7.1", + "serialize-javascript": "^7.0.2", "tar": "^7.4.3", + "tinypool": "^2.0.0", "tmp": "^0.2.3", "ts-essentials": "^10.1.1", "tslog": "^4.9.3", "web-tree-sitter": "^0.24.7", "ws": "^8.18.0", + "wtfnode": "^0.10.1", "xpath-ts2": "^1.4.2" } } diff --git a/src/config.ts b/src/config.ts index 003fc07160e..5d8dacd64bb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,6 +7,8 @@ import Joi from 'joi'; import type { BuiltInDefinitions } from './dataflow/environments/built-in-config'; import type { KnownParser } from './r-bridge/parser'; import type { DeepWritable } from 'ts-essentials'; +import type { WorkerPoolSettings } from './dataflow/parallel/threadpool'; +import { WorkerpoolDefaultSettings } from './dataflow/parallel/threadpool'; export enum VariableResolve { /** Don't resolve constants at all */ @@ -188,6 +190,19 @@ export interface FlowrConfigOptions extends MergeableRecord { } } } + + readonly optimizations: { + readonly fileParallelization: boolean; + + readonly dataflowOperationParallelization: boolean; + + readonly deferredFunctionEvaluation: boolean; + } + + readonly workerPool: { + + readonly poolSettings: WorkerPoolSettings; + } } export interface TreeSitterEngineConfig extends MergeableRecord { @@ -261,6 +276,14 @@ export const defaultConfigOptions: FlowrConfigOptions = { maxReadLines: 1e6 } } + }, + optimizations: { + fileParallelization: false, + dataflowOperationParallelization: false, + deferredFunctionEvaluation: false, + }, + workerPool: { + poolSettings: WorkerpoolDefaultSettings } }; @@ -359,6 +382,7 @@ export function cloneConfig(config: FlowrConfigOptions): FlowrConfigOptions { return JSON.parse(JSON.stringify(config)) as FlowrConfigOptions; } + /** * Loads the flowr config from the given file or the default locations. */ diff --git a/src/core/steps/all/core/20-dataflow.ts b/src/core/steps/all/core/20-dataflow.ts index fb4dc34caac..39547df0924 100644 --- a/src/core/steps/all/core/20-dataflow.ts +++ b/src/core/steps/all/core/20-dataflow.ts @@ -26,8 +26,15 @@ const staticDataflowCommon = { dependencies: [ 'normalize' ], } as const; -function processor(results: { normalize?: NormalizedAst }, input: { parser?: Parser, context?: FlowrAnalyzerContext }) { - return produceDataFlowGraph(input.parser as Parser, results.normalize as NormalizedAst, input.context as FlowrAnalyzerContext); +function processor( + results: { normalize?: NormalizedAst }, + input: { parser?: Parser, + context?: FlowrAnalyzerContext,}) { + return produceDataFlowGraph( + input.parser as Parser, + results.normalize as NormalizedAst, + input.context as FlowrAnalyzerContext, + ); } export const STATIC_DATAFLOW = { diff --git a/src/dataflow/environments/built-in.ts b/src/dataflow/environments/built-in.ts index 6421067d8d1..732ee82e590 100644 --- a/src/dataflow/environments/built-in.ts +++ b/src/dataflow/environments/built-in.ts @@ -44,7 +44,7 @@ import type { BuiltInFunctionDefinition, BuiltInReplacementDefinition } from './built-in-config'; -import type { ReadOnlyFlowrAnalyzerContext } from '../../project/context/flowr-analyzer-context'; +import type { FlowrAnalyzerContext, ReadOnlyFlowrAnalyzerContext } from '../../project/context/flowr-analyzer-context'; export type BuiltIn = `built-in:${string}`; @@ -212,6 +212,88 @@ export type ConfigOfBuiltInMappingName = Parameter export type BuiltInMemory = Map +type SerializedBuiltInEntry = { + __kind: 'builtin-entry'; + name: Identifier; + //definedAt: BuiltIn; +}; + +type SerializedIdentifierDefinition = + | IdentifierDefinition + | SerializedBuiltInEntry; + +export type SerializedBuiltInMemory = + Map; + +function isPureBuiltInEntry( + defs: readonly IdentifierDefinition[] +): boolean { + return defs.every( + d => + (d.type === ReferenceType.BuiltInFunction || + d.type === ReferenceType.BuiltInConstant) && + isBuiltIn(d.definedAt) + ); +} + +function isSerializedBuiltinEntry( + def: SerializedIdentifierDefinition +): def is SerializedBuiltInEntry { + return ( + typeof def === 'object' && + def !== null && + (def as SerializedBuiltInEntry).__kind === 'builtin-entry' + ); +} + + +/** + * + */ +export function serializeBuiltInMemory(mem: BuiltInMemory): SerializedBuiltInMemory{ + const serMem = new Map(); + + for(const [id, defs] of mem.entries()){ + if(isPureBuiltInEntry(defs)){ + /** just save the name, to recover later */ + serMem.set(id, [{ + __kind: 'builtin-entry', + name: id, + }]); + } else { + /** handle normally */ + serMem.set(id, defs); + } + } + return serMem; +} + +/** + * + */ +export function deserializeBuiltInMemory( + mem: SerializedBuiltInMemory, + ctx: FlowrAnalyzerContext +): BuiltInMemory { + const deSerMem = new Map(); + for( const [id,defs] of mem.entries()){ + if(defs.length === 1 && isSerializedBuiltinEntry(defs[0])){ + const builtInDefs = ctx.env.builtInEnvironment.memory.get(defs[0].name); + + if(!builtInDefs){ + console.warn('Could not recover builtin defs entry: ', defs[0]); + continue; + } + // convert to mutable data and clone to sever connection to readonly object + deSerMem.set(id, defs.map(def => ({ ...def })) as IdentifierDefinition[]); + } else { + deSerMem.set(id, defs as IdentifierDefinition[]); + } + + } + return deSerMem; +} + export class BuiltIns { /** * Register a built-in constant (like `NULL` or `TRUE`) to the given {@link builtIns} diff --git a/src/dataflow/environments/environment.ts b/src/dataflow/environments/environment.ts index b78ab22a6ab..2130bc3382f 100644 --- a/src/dataflow/environments/environment.ts +++ b/src/dataflow/environments/environment.ts @@ -4,7 +4,7 @@ * @module */ import { jsonReplacer } from '../../util/json'; -import type { BuiltInMemory } from './built-in'; +import { deserializeBuiltInMemory, serializeBuiltInMemory, type BuiltInMemory } from './built-in'; import type { Identifier, IdentifierDefinition, InGraphIdentifierDefinition } from './identifier'; import { guard } from '../../util/assert'; import type { ControlDependency } from '../info'; @@ -13,6 +13,8 @@ import type { FlowrConfigOptions } from '../../config'; import { mergeDefinitionsForPointer } from './define'; import { uniqueMergeValuesInDefinitions } from './append'; import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'; +import * as v8 from 'v8'; +import type { FlowrAnalyzerContext } from '../../project/context/flowr-analyzer-context'; /** A single entry/scope within an {@link REnvironmentInformation} */ export interface IEnvironment { @@ -232,18 +234,84 @@ export class Environment implements IEnvironment { return newEnv; } - toJSON(): Jsonified { + public toJSON(excludeBuiltIn = false): Jsonified { return this.builtInEnv ? { id: this.id, - parent: this.parent, + parent: this.parent ? this.parent.toJSON(excludeBuiltIn) : undefined, // walk up the chain builtInEnv: this.builtInEnv, - memory: this.memory, + memory: excludeBuiltIn ? undefined as unknown as BuiltInMemory: this.memory, // discard if this is a builtin env } : { id: this.id, - parent: this.parent, + parent: this.parent.toJSON(excludeBuiltIn), memory: this.memory, }; } + + public toSerializable(): Uint8Array { + try { + const json = this.toJSON(true); + if(this.builtInEnv){ + // erase content, as we will restore this later + json.memory = undefined as unknown as BuiltInMemory; + } + /** replace all memory with string representation */ + let current = json; + while(current.parent){ + current.memory = serializeBuiltInMemory(current.memory) as BuiltInMemory; + current = current.parent; + } + + return v8.serialize(json); + } catch(err: unknown) { + console.warn('Failed to serialize env:', err); + return new Uint8Array(); + } + } + + public static fromSerializable(data: Uint8Array, ctx?: FlowrAnalyzerContext): Environment { + if(!ctx) { + throw new Error('deserialization for env failed, as no ctx was provided'); + } + try { + const json = v8.deserialize(data) as Jsonified; + + /** rebuild all memory */ + let current = json; + while(current.parent){ + current.memory = deserializeBuiltInMemory(current.memory, ctx); + current = current.parent; + } + return this.fromJSON(json, ctx); + } catch(err) { + console.warn('Failed to deserialize env:', err); + return new Environment(undefined as unknown as Environment); + } + } + + public static fromJSON(json: Jsonified, ctx?: FlowrAnalyzerContext): Environment { + if(!json){ + console.warn('Failed to deserialize env json as nothing was provided'); + return new Environment(undefined as unknown as Environment); + } + + // handle the builtin env + if(json.builtInEnv){ + if(!ctx){ + console.error('Failed to deserialize builtin env as no context was provided'); + return new Environment(undefined as unknown as Environment, true); + } + // just retrieve from FlowrAnalyzerContext -> FlowrEnvironmentContext + return ctx.env.builtInEnvironment as Environment; + } + const parent = json.parent ? Environment.fromJSON(json.parent, ctx) : undefined as unknown as Environment; + if(!parent && json.parent){ + console.warn('Env Parent could not be deserialized'); + } + + const env = new Environment(parent); + env.memory = json.memory instanceof Map ? json.memory : new Map(json.memory); + return env; + } } /** @@ -273,6 +341,34 @@ export interface REnvironmentInformation { readonly level: number } +export interface SerializedREnvironmentInformation { + readonly current: Uint8Array; + readonly level: number; +} + +/** + * + */ +export function toSerializedREnvironmentInformation(env: REnvironmentInformation): SerializedREnvironmentInformation { + return { + level: env.level, + current: env.current.toSerializable() + }; +} + +/** + * + */ +export function fromSerializedREnvironmentInformation( + data: SerializedREnvironmentInformation, + flowrContext?: FlowrAnalyzerContext +): REnvironmentInformation { + return { + level: data.level, + current: Environment.fromSerializable(data.current, flowrContext) + }; +} + /** * Helps to serialize an environment, but replaces the built-in environment with a placeholder. */ @@ -284,4 +380,175 @@ export function builtInEnvJsonReplacer(k: unknown, v: unknown): unknown { } } +export interface EnvironmentDiff { + isEqual: boolean; + issues: EnvironmentIssue[]; +} + +export interface EnvironmentIssue { + path: string; + kind: EnvironmentIssueType; + comment?: string; +} + +export type EnvironmentIssueType = 'type-mismatch' + | 'value-mismatch' + | 'missing-key' + | 'extra-key' + | 'map-size-mismatch' + | 'set-size-mismatch' + | 'function-found' + | 'builtin-mismatch'; + +/** + * + */ +export function diffEnvironments(a: Environment, b: Environment): EnvironmentDiff { + const issues: EnvironmentIssue[] = []; + + // Built-in envs must match by identity + if(a.builtInEnv || b.builtInEnv) { + if(a !== b) { + issues.push({ + path: 'env', + kind: 'builtin-mismatch' + }); + } + return { + isEqual: issues.length === 0, + issues + }; + } + + // Compare memory + diffDeep(a.memory, b.memory, 'memory', issues); + + // Compare parents recursively + if(a.parent || b.parent) { + if(!a.parent || !b.parent) { + issues.push({ + path: 'parent', + kind: 'value-mismatch' + }); + } else { + const parentDiff = diffEnvironments(a.parent, b.parent); + issues.push( + ...parentDiff.issues.map(i => ({ + ...i, + path: `parent.${i.path}` + })) + ); + } + } + + return { + isEqual: issues.length === 0, + issues + }; +} + + +function diffDeep( + a: unknown, + b: unknown, + path: string, + issues: EnvironmentIssue[] +): void { + if(a === b) { + return; + } + + if(typeof a !== typeof b) { + issues.push({ path, kind: 'type-mismatch' }); + return; + } + if(typeof a === 'function' || typeof b === 'function') { + issues.push({ path, kind: 'function-found' }); + return; + } + + if(a === null || b === null) { + if(a !== b) { + issues.push({ path, kind: 'value-mismatch' }); + } + return; + } + + if(Array.isArray(a)) { + if(!Array.isArray(b)) { + issues.push({ path, kind: 'type-mismatch' }); + return; + } + if(a.length !== b.length) { + issues.push({ path, kind: 'value-mismatch' }); + } + a.forEach((v, i) => + diffDeep(v, b[i], `${path}[${i}]`, issues) + ); + return; + } + + if(a instanceof Map) { + if(!(b instanceof Map)) { + issues.push({ path, kind: 'type-mismatch' }); + return; + } + if(a.size !== b.size) { + issues.push({ path, kind: 'map-size-mismatch' }); + } + for(const [k, v] of a) { + if(!b.has(k)) { + issues.push({ path: `${path}.${String(k)}`, kind: 'missing-key' }); + } else { + diffDeep(v, b.get(k), `${path}.${String(k)}`, issues); + } + } + for(const k of b.keys()) { + if(!a.has(k)) { + issues.push({ path: `${path}.${String(k)}`, kind: 'extra-key' }); + } + } + return; + } + + if(a instanceof Set) { + if(!(b instanceof Set)) { + issues.push({ path, kind: 'type-mismatch' }); + return; + } + if(a.size !== b.size) { + issues.push({ path, kind: 'set-size-mismatch' }); + } + return; + } + + if(typeof a === 'object') { + const ak = Object.keys(a); + const bk = Object.keys(b as object); + + for(const k of ak) { + if(!(k in (b as object))) { + issues.push({ path: `${path}.${k}`, kind: 'missing-key' }); + } else { + diffDeep( + (a as Record)[k], + (b as Record)[k], + `${path}.${k}`, + issues + ); + } + } + + for(const k of bk) { + if(!(k in (a))) { + issues.push({ path: `${path}.${k}`, kind: 'extra-key' }); + } + } + return; + } + + if(a !== b) { + issues.push({ path, kind: 'value-mismatch' }); + } +} diff --git a/src/dataflow/extractor.ts b/src/dataflow/extractor.ts index 767a5f2e92d..a856398e710 100644 --- a/src/dataflow/extractor.ts +++ b/src/dataflow/extractor.ts @@ -1,5 +1,5 @@ -import type { DataflowInformation } from './info'; -import { type DataflowProcessorInformation, type DataflowProcessors, processDataflowFor } from './processor'; +import { DeserializeDataflowInformation, type DataflowInformation } from './info'; +import { type DataflowProcessorInformation, type DataflowProcessors, DeserializeDataflowProcessorInformation, processDataflowFor, SerializeDataflowProcessorInformation } from './processor'; import { processUninterestingLeaf } from './internal/process/process-uninteresting-leaf'; import { processSymbol } from './internal/process/process-symbol'; import { processFunctionCall } from './internal/process/functions/call/default-call-handling'; @@ -12,10 +12,10 @@ import { wrapArgumentsUnnamed } from './internal/process/functions/call/argument import { rangeFrom } from '../util/range'; import type { NormalizedAst, ParentInformation } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; import { RType } from '../r-bridge/lang-4.x/ast/model/type'; -import { standaloneSourceFile } from './internal/process/functions/call/built-in/built-in-source'; +import { mergeDataflowInformation, standaloneSourceFile } from './internal/process/functions/call/built-in/built-in-source'; import type { DataflowGraph } from './graph/graph'; import { extractCfgQuick, getCallsInCfg } from '../control-flow/extract-cfg'; -import { EdgeType } from './graph/edge'; +import { edgeIncludesType, EdgeType } from './graph/edge'; import { identifyLinkToLastCallRelation } from '../queries/catalog/call-context-query/identify-link-to-last-call-relation'; @@ -24,8 +24,96 @@ import { updateNestedFunctionCalls } from './internal/process/functions/call/bui import type { ControlFlowInformation } from '../control-flow/control-flow-graph'; import type { FlowrAnalyzerContext } from '../project/context/flowr-analyzer-context'; import { FlowrFile } from '../project/context/flowr-file'; -import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id'; -import type { DataflowGraphVertexFunctionCall } from './graph/vertex'; +import { recoverName, type NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id'; +import { VertexType, type DataflowGraphVertexFunctionCall } from './graph/vertex'; +import { dataflowLogger } from './logger'; +import type { DataflowPayload, DataflowReturnPayload } from './parallel/task-registry'; +import type { REnvironmentInformation } from './environments/environment'; +import { linkInputs } from './internal/linker'; +import type { IdentifierReference } from './environments/identifier'; +import { ReferenceType } from './environments/identifier'; +import { isBuiltIn } from './environments/built-in'; + +function snapshotBuiltInDefinitions(environment: REnvironmentInformation): Set { + let current = environment.current; + while(!current.builtInEnv) { + current = current.parent; + } + + return new Set(current.memory.keys()); +} + +function scanRedefinedBuiltIns( + environment: REnvironmentInformation, + builtInDefinitions: ReadonlySet, + knownRedefinitions?: ReadonlySet +): Set { + const redefined = knownRedefinitions ? new Set(knownRedefinitions) : new Set(); + let current = environment.current; + + while(!current.builtInEnv) { + for(const [name, definitions] of current.memory.entries()) { + if(redefined.has(name) || !builtInDefinitions.has(name) || definitions.length === 0) { + continue; + } + + if(definitions.some(definition => !isBuiltIn(definition.definedAt))) { + redefined.add(name); + } + } + + current = current.parent; + } + + return redefined; +} + +function findUsedRedefinedBuiltIns( + graph: DataflowGraph, + redefinedBuiltIns: ReadonlySet +): Set { + const used = new Set(); + + if(redefinedBuiltIns.size === 0) { + return used; + } + + for(const [, vertex] of graph.vertices(true)) { + if(vertex.tag === VertexType.FunctionCall && redefinedBuiltIns.has(vertex.name)) { + used.add(vertex.name); + } + } + + return used; +} + +function collectFunctionCallCandidates(targetGraph: DataflowGraph): IdentifierReference[] { + const candidates: IdentifierReference[] = []; + const seenNodeIds = new Set(); + const hasBuiltInCallEdge = (nodeId: NodeId): boolean => + [...(targetGraph.outgoingEdges(nodeId)?.entries() ?? [])] + .some(([target, edge]) => edgeIncludesType(edge.types, EdgeType.Calls) && isBuiltIn(target)); + + for(const [nodeId, vertex] of targetGraph.vertices(true)) { + if(vertex.tag !== VertexType.FunctionCall || seenNodeIds.has(nodeId)) { + continue; + } + + if(isBuiltIn(vertex.name) || hasBuiltInCallEdge(nodeId)) { + continue; + } + + seenNodeIds.add(nodeId); + candidates.push({ + nodeId, + name: recoverName(nodeId, targetGraph.idMap) ?? vertex.name, + controlDependencies: vertex.cds, + type: ReferenceType.Function + }); + } + + return candidates; +} /** * The best friend of {@link produceDataFlowGraph} and {@link processDataflowFor}. @@ -96,23 +184,106 @@ function resolveLinkToSideEffects(ast: NormalizedAst, graph: DataflowGraph) { return cf; } +function resolveCrossFileReferences( + graph: DataflowGraph, + environment: REnvironmentInformation, + functionCallCandidates: readonly IdentifierReference[], +): void { + const asIdentifierReference = ( + nodeId: NodeId, + controlDependencies: IdentifierReference['controlDependencies'], + type: ReferenceType.Variable | ReferenceType.Function, + fallbackName?: string + ): IdentifierReference => ({ + nodeId, + name: recoverName(nodeId, graph.idMap) ?? fallbackName, + controlDependencies, + type + }); + + const _clearReadsFromFunctionCalls = (calls: readonly IdentifierReference[]): void => { + for(const call of calls) { + const outgoingEdges = graph.outgoingEdges(call.nodeId); + if(outgoingEdges === undefined) { + continue; + } + + for(const [target, edge] of [...outgoingEdges.entries()]) { + if(!edgeIncludesType(edge.types, EdgeType.Reads)) { + continue; + } + + edge.types &= ~EdgeType.Reads; + if(edge.types === 0) { + graph.removeEdge(call.nodeId, target); + } + } + } + }; + + /** collect use references that may have builtin upgrades */ + const useCandidates: IdentifierReference[] = []; + + for(const [nodeId, vertex] of graph.vertices(true)) { + if(vertex.tag !== VertexType.Use) { + continue; + } + + const readTargets = [...(graph.outgoingEdges(nodeId)?.entries() ?? [])] + .filter(([, edge]) => edgeIncludesType(edge.types, EdgeType.Reads)) + .map(([target]) => target); + const hasReads = readTargets.length > 0; + + if(!hasReads) { + useCandidates.push(asIdentifierReference(nodeId, vertex.cds, ReferenceType.Variable)); + continue; + } + + // For Use references, only process if reads are to builtins (to upgrade them) + if(readTargets.every(target => isBuiltIn(target))) { + useCandidates.push(asIdentifierReference(nodeId, vertex.cds, ReferenceType.Variable)); + } + } + + // Reset function-call reads and relink them against the current merged environment. + //clearReadsFromFunctionCalls(functionCallCandidates); + + // Resolve function calls without builtin placeholder upgrading. + const unresolvedFunctions = linkInputs(functionCallCandidates, environment, [], graph, false, false); + + // Resolve use references with builtin placeholder upgrading + const unresolvedUses = linkInputs(useCandidates, environment, [], graph, false, true); + + const unresolved = unresolvedFunctions.concat(unresolvedUses); + //const unresolved = unresolvedUses; + + if(unresolved.length > 0){ + console.warn(`Cross File Resolution: ${unresolved.length} reference(s) remain unresolved across all files for the dataflow graph: ` + + unresolved.map(reference => reference.name ?? '').join(',') + ); + } +} + /** * This is the main function to produce the dataflow graph from a given request and normalized AST. * Note, that this requires knowledge of the active parser in case the dataflow analysis uncovers other files that have to be parsed and integrated into the analysis * (e.g., in the event of a `source` call). * For the actual, canonical fold entry point, see {@link processDataflowFor}. */ -export function produceDataFlowGraph( +export async function produceDataFlowGraph( parser: Parser, completeAst: NormalizedAst, - ctx: FlowrAnalyzerContext -): DataflowInformation & { cfgQuick: ControlFlowInformation | undefined } { + ctx: FlowrAnalyzerContext, +): Promise { // we freeze the files here to avoid endless modifications during processing const files = completeAst.ast.files.slice(); ctx.files.addConsideredFile(files[0].filePath ? files[0].filePath : FlowrFile.INLINE_PATH); + const fileParallelization = ctx.config.optimizations.fileParallelization; + const workerPool = fileParallelization ? ctx.workerPool : undefined; + const dfData: DataflowProcessorInformation = { parser, completeAst, @@ -122,6 +293,90 @@ export function produceDataFlowGraph( referenceChain: [files[0].filePath], ctx }; + + if(fileParallelization && workerPool){ + const builtInDefinitions = snapshotBuiltInDefinitions(dfData.environment); + + // parse data + const parsed = SerializeDataflowProcessorInformation(dfData); + const result = await workerPool.submitTasks, DataflowReturnPayload>( + 'parallelFiles', + files.map((file, i) => ({ + index: i, + file, + data: parsed, + })) + ); + + const parsedResult = result.map(data => { + const dfInfo = DeserializeDataflowProcessorInformation(data.processorInfo, dfData.processors, dfData.parser); + const dataflow = DeserializeDataflowInformation(data.dataflowData, dfInfo.ctx); + return { + processorInfo: dfInfo, + dataflow: dataflow, + }; + }); + + let df = parsedResult[0].dataflow; + console.log('result length: ', parsedResult.length); + let shouldFallbackToSequential = false; + let trackedRedefinedBuiltIns; + let fallbackIteration: number | undefined; + + // merge dataflowinformation via folding + for(let i = 1; i < result.length; i++){ + console.log('merging dataflow for file-', i); + const functionCallCandidates = new Map(); + for(const candidate of collectFunctionCallCandidates(df.graph)) { + functionCallCandidates.set(candidate.nodeId, candidate); + } + for(const candidate of collectFunctionCallCandidates(parsedResult[i].dataflow.graph)) { + functionCallCandidates.set(candidate.nodeId, candidate); + } + + df = mergeDataflowInformation('file-'+i, parsedResult[i].processorInfo, files[i].filePath, df, parsedResult[i].dataflow); + + trackedRedefinedBuiltIns = scanRedefinedBuiltIns(df.environment, builtInDefinitions, trackedRedefinedBuiltIns); + + const usedRedefinedBuiltIns = findUsedRedefinedBuiltIns(df.graph, trackedRedefinedBuiltIns); + if(usedRedefinedBuiltIns.size > 0) { + console.warn( + `Dataflow:: Detected usage of redefined built-ins in merged graph (${[...usedRedefinedBuiltIns].join(', ')}). ` + + 'Aborting parallel merge and falling back to sequential computation.' + ); + shouldFallbackToSequential = true; + fallbackIteration = i; + break; + } + console.log('pre resolve ',df.graph.outgoingEdges(33)); + resolveCrossFileReferences(df.graph, df.environment, [...functionCallCandidates.values()]); + console.log('post resolve ',df.graph.outgoingEdges(33)); + } + + if(shouldFallbackToSequential) { + // Mark debug information before falling through to sequential analysis + if(fallbackIteration !== undefined) { + df.reanalysisTriggered = true; + df.reanalysisIteration = fallbackIteration; + df.reanalysisFileIndex = fallbackIteration; + } + // fall through to sequential analysis below + } else { + + // finally, resolve linkages + updateNestedFunctionCalls(df.graph, df.environment); + + const cfgQuick = resolveLinkToSideEffects(completeAst, df.graph); + + // performance optimization: return cfgQuick as part of the result to avoid recomputation + return { ...df, cfgQuick }; + } + } + + if(!workerPool && fileParallelization){ + dataflowLogger.error('Dataflow:: Parallelization is enabled, but no Threadpool is provided. Falling back to sequential computation.'); + } + // use the sequential analysis let df = processDataflowFor(files[0].root, dfData); for(let i = 1; i < files.length; i++) { @@ -136,4 +391,5 @@ export function produceDataFlowGraph( // performance optimization: return cfgQuick as part of the result to avoid recomputation return { ...df, cfgQuick }; + } diff --git a/src/dataflow/graph/diff-dataflow-graph.ts b/src/dataflow/graph/diff-dataflow-graph.ts index 5ad04060dce..d62959b9c05 100644 --- a/src/dataflow/graph/diff-dataflow-graph.ts +++ b/src/dataflow/graph/diff-dataflow-graph.ts @@ -259,7 +259,13 @@ function diffEdge(edge: DataflowGraphEdge, otherEdge: DataflowGraphEdge, ctx: Gr { tag: 'edge', from: id, to: target } ); } - if(edge.types !== otherEdge.types) { + + const onlyInLeft = edge.types & ~otherEdge.types; + const onlyInRight = otherEdge.types & ~edge.types; + const hasForbiddenDifference = (onlyInLeft !== 0 && !ctx.config.rightIsSubgraph) + || (onlyInRight !== 0 && !ctx.config.leftIsSubgraph); + + if(hasForbiddenDifference) { ctx.report.addComment( `Target of ${id}->${target} in ${ctx.leftname} differs in edge types: ${JSON.stringify([...edgeTypesToNames(edge.types)])} vs ${JSON.stringify([...edgeTypesToNames(otherEdge.types)])}`, { tag: 'edge', from: id, to: target } diff --git a/src/dataflow/graph/graph.ts b/src/dataflow/graph/graph.ts index b7153c70f1e..7806114d246 100644 --- a/src/dataflow/graph/graph.ts +++ b/src/dataflow/graph/graph.ts @@ -14,20 +14,53 @@ import { uniqueArrayMerge } from '../../util/collections/arrays'; import { EmptyArgument } from '../../r-bridge/lang-4.x/ast/model/nodes/r-function-call'; import type { Identifier, IdentifierDefinition, IdentifierReference } from '../environments/identifier'; import { type NodeId, normalizeIdToNumberIfPossible } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'; -import { Environment , type IEnvironment, type REnvironmentInformation } from '../environments/environment'; -import type { AstIdMap } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import { type SerializedREnvironmentInformation , Environment, fromSerializedREnvironmentInformation, toSerializedREnvironmentInformation, type IEnvironment, type REnvironmentInformation } from '../environments/environment'; +import type { RNodeWithParent , AstIdMap, SerializableAstIdMap } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; import { cloneEnvironmentInformation } from '../environments/clone'; import { jsonReplacer } from '../../util/json'; import { dataflowLogger } from '../logger'; import type { LinkTo } from '../../queries/catalog/call-context-query/call-context-query-format'; import type { Writable } from 'ts-essentials'; import type { BuiltInMemory } from '../environments/built-in'; +import * as v8 from 'v8'; +import { BiMap } from '../../util/collections/bimap'; +import type { FlowrAnalyzerContext } from '../../project/context/flowr-analyzer-context'; /** * Describes the information we store per function body. * The {@link DataflowFunctionFlowInformation#exitPoints} are stored within the enclosing {@link DataflowGraphVertexFunctionDefinition} vertex. */ -export type DataflowFunctionFlowInformation = Omit & { graph: Set } +export type DataflowFunctionFlowInformation = Omit & { graph: Set } + +export type DataflowFunctionFlowInformationSerialized = Omit & { + environment: SerializedREnvironmentInformation | undefined; +} + +/** + * + */ +export function serializeSubflow(subflow: DataflowFunctionFlowInformation): DataflowFunctionFlowInformationSerialized { + const serializedEnv = subflow.environment ? toSerializedREnvironmentInformation(subflow.environment) + : undefined; + return { + ...subflow, + environment: serializedEnv, + }; +} + +/** + * + */ +export function deserializeSubflow( + subflow: DataflowFunctionFlowInformationSerialized, + ctx?: FlowrAnalyzerContext): DataflowFunctionFlowInformation { + const env = subflow.environment ? fromSerializedREnvironmentInformation(subflow.environment, ctx) + : undefined; + return { + ...subflow, + environment: env, + } as DataflowFunctionFlowInformation; +} /** * A reference with a name, e.g. `a` and `b` in the following function call: @@ -39,7 +72,7 @@ export type DataflowFunctionFlowInformation = Omit { - readonly name?: undefined + readonly name?: undefined } /** Summarizes either named (`foo(a = 3, b = 2)`), unnamed (`foo(3, 2)`), or empty (`foo(,)`) arguments within a function. */ @@ -85,7 +118,7 @@ export function getReferenceOfArgument(arg: FunctionArgument): NodeId | undefine /** * A reference that is enough to indicate start and end points of an edge within the dataflow graph. */ -type ReferenceForEdge = Pick | IdentifierDefinition +type ReferenceForEdge = Pick | IdentifierDefinition /** @@ -102,12 +135,30 @@ export type IngoingEdges = M * The structure of the serialized {@link DataflowGraph}. */ export interface DataflowGraphJson { - readonly rootVertices: NodeId[], - readonly vertexInformation: [NodeId, DataflowGraphVertexInfo][], - readonly edgeInformation: [NodeId, [NodeId, DataflowGraphEdge][]][] - readonly _unknownSideEffects: UnknownSideEffect[] + readonly rootVertices: NodeId[]; + readonly vertexInformation: [NodeId, DataflowGraphVertexInfo][]; + readonly edgeInformation: [NodeId, [NodeId, DataflowGraphEdge][]][]; + readonly _unknownSideEffects: UnknownSideEffect[]; } +/** + * The serialized form of a {@link DataflowGraph}. + * Includes the json representation as well as an optional id-map to the normalized AST. + */ +interface SerializableDataflowGraph { + json: DataflowGraphJson; + idMap?: SerializableAstIdMap; +} + +type PersistedVertex = Omit & { + environment?: SerializedREnvironmentInformation; + subflow?: DataflowFunctionFlowInformationSerialized; +}; + +type PersistedVertexEntry = [NodeId, PersistedVertex]; + + + /** * An unknown side effect describes something that we cannot handle correctly (in all cases). * For example, `load` will be marked as an unknown side effect as we have no idea of how it will affect the program. @@ -132,16 +183,16 @@ export type UnknownSideEffect = NodeId | { id: NodeId, linkTo: LinkTo } * @see {@link emptyGraph|`emptyGraph`} - to create an empty graph (useful in tests) */ export class DataflowGraph< - Vertex extends DataflowGraphVertexInfo = DataflowGraphVertexInfo, - Edge extends DataflowGraphEdge = DataflowGraphEdge + Vertex extends DataflowGraphVertexInfo = DataflowGraphVertexInfo, + Edge extends DataflowGraphEdge = DataflowGraphEdge > { private _idMap: AstIdMap | undefined; /* - * Set of vertices which have sideEffects that we do not know anything about. - * As a (temporary) solution until we have FD edges, a side effect may also store known target links - * that have to be/should be resolved (as globals) as a separate pass before the df analysis ends. - */ + * Set of vertices which have sideEffects that we do not know anything about. + * As a (temporary) solution until we have FD edges, a side effect may also store known target links + * that have to be/should be resolved (as globals) as a separate pass before the df analysis ends. + */ private readonly _unknownSideEffects = new Set(); constructor(idMap: AstIdMap | undefined) { @@ -157,6 +208,18 @@ export class DataflowGraph< private types: Map = new Map(); + private rebuildTypeIndex(): void { + this.types = new Map(); + for(const [id, vertex] of this.vertexInformation.entries()) { + const existing = this.types.get(vertex.tag); + if(existing) { + existing.push(id); + } else { + this.types.set(vertex.tag, [id]); + } + } + } + toJSON(): DataflowGraphJson { return { @@ -167,6 +230,7 @@ export class DataflowGraph< }; } + /** * Get the {@link DataflowGraphVertexInfo} attached to a node as well as all outgoing edges. * @param id - The id of the node to get @@ -249,7 +313,7 @@ export class DataflowGraph< * @returns the ids of all toplevel vertices in the graph together with their vertex information * @see #edges */ - public* vertices(includeDefinedFunctions: boolean): MapIterator<[NodeId, Vertex]> { + public * vertices(includeDefinedFunctions: boolean): MapIterator<[NodeId, Vertex]> { if(includeDefinedFunctions) { yield* this.vertexInformation.entries(); } else { @@ -259,7 +323,7 @@ export class DataflowGraph< } } - public* verticesOfType(type: T): MapIterator<[NodeId, Vertex & { tag: T }]> { + public * verticesOfType(type: T): MapIterator<[NodeId, Vertex & { tag: T }]> { const ids = this.types.get(type) ?? []; for(const id of ids) { yield [id, this.vertexInformation.get(id) as Vertex & { tag: T }]; @@ -274,7 +338,7 @@ export class DataflowGraph< * @returns the ids of all edges in the graph together with their edge information * @see #vertices */ - public* edges(): MapIterator<[NodeId, OutgoingEdges]> { + public * edges(): MapIterator<[NodeId, OutgoingEdges]> { yield* this.edgeInformation.entries(); } @@ -322,7 +386,7 @@ export class DataflowGraph< ...vertex, environment } as unknown as Vertex); - const has = this.types.get(vertex.tag); + const has = this.types.get(vertex.tag); if(has) { has.push(vertex.id); } else { @@ -367,6 +431,22 @@ export class DataflowGraph< return this; } + /** + * Removes an edge from the graph. + * @param from - The source node ID + * @param to - The target node ID + */ + public removeEdge(from: NodeId, to: NodeId): this { + const edges = this.edgeInformation.get(from); + if(edges !== undefined) { + edges.delete(to); + if(edges.size === 0) { + this.edgeInformation.delete(from); + } + } + return this; + } + /** * Merges the other graph into *this* one (in-place). The return value is only for convenience. * @param otherGraph - The graph to merge into this one @@ -430,6 +510,8 @@ export class DataflowGraph< if(vertex.tag === VertexType.FunctionDefinition || vertex.tag === VertexType.VariableDefinition) { vertex.cds = reference.controlDependencies; } else { + this.types.set(vertex.tag, (this.types.get(vertex.tag) ?? []).filter(id => id !== reference.nodeId)); + this.types.set(VertexType.VariableDefinition, (this.types.get(VertexType.VariableDefinition) ?? []).concat([reference.nodeId])); this.vertexInformation.set(reference.nodeId, { ...vertex, tag: VertexType.VariableDefinition }); } } @@ -490,6 +572,7 @@ export class DataflowGraph< const graph = new DataflowGraph(undefined); graph.rootVertices = new Set(data.rootVertices); graph.vertexInformation = new Map(data.vertexInformation); + graph.rebuildTypeIndex(); for(const [, vertex] of graph.vertexInformation) { if(vertex.environment) { (vertex.environment as Writable) = renvFromJson(vertex.environment as unknown as REnvironmentInformationJson); @@ -501,6 +584,146 @@ export class DataflowGraph< } return graph; } + + private static serializeVerticesForPersistence( + vertices: [NodeId, DataflowGraphVertexInfo][] + ): PersistedVertexEntry[] { + return vertices.map(([id, vertex]) => { + const serializedEnv = vertex.environment + ? toSerializedREnvironmentInformation(vertex.environment) + : undefined; + + if(vertex.tag === VertexType.FunctionDefinition) { + const fn = vertex as DataflowGraphVertexFunctionDefinition; + return [id, { + ...vertex, + environment: serializedEnv, + subflow: serializeSubflow(fn.subflow) + }]; + } + + return [id, { + ...vertex, + environment: serializedEnv + }]; + }); + } + + + private static stripSerializedEnvironments( + json: DataflowGraphJson + ): DataflowGraphJson { + return { + ...json, + vertexInformation: json.vertexInformation.map(([id, v]) => [ + id, + ({ ...v, environment: undefined } as unknown as DataflowGraphVertexInfo) + ]) + }; + } + + private static reattachSerializedVertexState( + graph: DataflowGraph, + serializedVertices: PersistedVertexEntry[], + ctx?: FlowrAnalyzerContext + ): void { + for(const [id, serializedVertex] of serializedVertices) { + const vertex = graph.getVertex(id, true); + if(!vertex) { + continue; + } + + // Restore environment + if(serializedVertex.environment) { + (vertex.environment as Writable) = + fromSerializedREnvironmentInformation(serializedVertex.environment, ctx); + } + + // Restore subflow for function definitions + if( + vertex.tag === VertexType.FunctionDefinition && + serializedVertex.subflow + ) { + (vertex as DataflowGraphVertexFunctionDefinition).subflow = + deserializeSubflow(serializedVertex.subflow, ctx); + } + } + } + + + /** + * Serializes the DataflowGraüh into simple Byte Data + * @returns Buffer containing the unsigned data bytes + */ + public toSerializable(): Uint8Array { + try { + const json = this.toJSON(); + + const persistedVertices = DataflowGraph.serializeVerticesForPersistence(json.vertexInformation); + + const payload: SerializableDataflowGraph = { + json: { + ...json, + vertexInformation: persistedVertices as unknown as [NodeId, DataflowGraphVertexInfo][] + }, + idMap: this._idMap?.toSerializable() + }; + + + return v8.serialize(payload); + } catch(err: unknown) { + console.log('Failed to serialize dataflow graph, err: ', err); + return new Uint8Array(); + } + } + + + /** + * Deserializes from byte data into identical DataflowGraph + * @param buffer - Buffer to reconstruct DataflowGraph from + * @returns DataflowGraph Instance + */ + public static fromSerializable( + buffer: Uint8Array, + ctx?: FlowrAnalyzerContext + ): DataflowGraph { + if(buffer.length === 0) { + dataflowLogger.warn('DataflowGraph: deserialize called with empty buffer.'); + return new DataflowGraph(undefined); + } + + try { + const payload = v8.deserialize(buffer) as SerializableDataflowGraph; + + const serializedJson = payload.json; + + // Prevent fromJson from seeing binary envs + const json = this.stripSerializedEnvironments(serializedJson); + + // legacy reconstruction only + const graph = DataflowGraph.fromJson(json); + + // restore AstIdMap + if(payload.idMap) { + (graph as unknown as { _idMap: AstIdMap | undefined })._idMap = BiMap.fromSerializable(payload.idMap); + } + + // reattach semantic state to actual graph vertices + DataflowGraph.reattachSerializedVertexState( + graph, + serializedJson.vertexInformation as unknown as PersistedVertexEntry[], + ctx + ); + + + return graph; + } catch{ + dataflowLogger.warn('DataflowGraph: deserialize failed on parsing'); + return new DataflowGraph(undefined); + } + } + + } function mergeNodeInfos(current: Vertex, next: Vertex): Vertex { @@ -538,8 +761,8 @@ export interface IEnvironmentJson { } interface REnvironmentInformationJson { - readonly current: IEnvironmentJson; - readonly level: number; + readonly current: IEnvironmentJson; + readonly level: number; } function envFromJson(json: IEnvironmentJson): Environment { diff --git a/src/dataflow/info.ts b/src/dataflow/info.ts index 263f08906a4..4eb93d0b1ed 100644 --- a/src/dataflow/info.ts +++ b/src/dataflow/info.ts @@ -1,9 +1,11 @@ -import type { DataflowProcessorInformation } from './processor'; +import { type DataflowProcessorInformation } from './processor'; import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id'; import type { IdentifierReference } from './environments/identifier'; -import type { REnvironmentInformation } from './environments/environment'; +import { fromSerializedREnvironmentInformation, toSerializedREnvironmentInformation, type REnvironmentInformation, type SerializedREnvironmentInformation } from './environments/environment'; import { DataflowGraph } from './graph/graph'; import type { GenericDifferenceInformation, WriteableDifferenceReport } from '../util/diff'; +import { dataflowLogger } from './logger'; +import type { FlowrAnalyzerContext } from '../project/context/flowr-analyzer-context'; /** @@ -73,6 +75,16 @@ export interface DataflowCfgInformation { exitPoints: readonly ExitPoint[] } +export interface SerializableDataflowInformation{ + entryPoint: NodeId; + exitPoints: readonly ExitPoint[]; + unknownReferences: readonly IdentifierReference[]; + in: readonly IdentifierReference[]; + out: readonly IdentifierReference[]; + env: SerializedREnvironmentInformation; + graph: Uint8Array; +} + /** * The dataflow information is one of the fundamental structures we have in the dataflow analysis. * It is continuously updated during the dataflow analysis @@ -91,21 +103,92 @@ export interface DataflowInformation extends DataflowCfgInformation { * as we have not yet seen the assignment! * @see {@link IdentifierReference} - a reference on a variable, parameter, function call, ... */ - unknownReferences: readonly IdentifierReference[] + unknownReferences: readonly IdentifierReference[] /** * References which are read within the current subtree. * @see {@link IdentifierReference} - a reference on a variable, parameter, function call, ... */ - in: readonly IdentifierReference[] + in: readonly IdentifierReference[] /** * References which are written to within the current subtree * @see {@link IdentifierReference} - a reference on a variable, parameter, function call, ... */ - out: readonly IdentifierReference[] + out: readonly IdentifierReference[] /** Current environments used for name resolution, probably updated on the next expression-list processing */ - environment: REnvironmentInformation + environment: REnvironmentInformation /** The current constructed dataflow graph */ - graph: DataflowGraph + graph: DataflowGraph + /** + * Debug information tracking parallel merge re-analysis behavior. + * Only set when file parallelization is enabled and a fallback occurs. + */ + reanalysisTriggered?: boolean + /** Which merge iteration triggered the re-analysis fallback (0-indexed) */ + reanalysisIteration?: number + /** Which file index was being merged when re-analysis was triggered (0-indexed) */ + reanalysisFileIndex?: number +} + +/** + * + */ +export function SerializeDataflowInformation(data: DataflowInformation): SerializableDataflowInformation { + try { + return { + entryPoint: data.entryPoint, + exitPoints: [...data.exitPoints], + unknownReferences: [...data.unknownReferences], + in: [...data.in], + out: [...data.out], + env: toSerializedREnvironmentInformation(data.environment), + graph: data.graph.toSerializable(), + }; + } catch(err: unknown) { + dataflowLogger.warn('Serialization of DataflowInformation failed with: ', err); + return { + entryPoint: undefined as unknown as NodeId, + exitPoints: [], + unknownReferences: [], + in: [], + out: [], + env: { + level: data.environment.level, + current: new Uint8Array(), + }, + graph: new Uint8Array(), + }; + } +} + +/** + * + */ +export function DeserializeDataflowInformation( + data: SerializableDataflowInformation, + ctx?: FlowrAnalyzerContext +): DataflowInformation { + try { + return { + entryPoint: data.entryPoint, + exitPoints: data.exitPoints, + unknownReferences: data.unknownReferences, + in: data.in, + out: data.out, + environment: fromSerializedREnvironmentInformation(data.env, ctx), + graph: DataflowGraph.fromSerializable(data.graph, ctx), + }; + } catch(err: unknown) { + dataflowLogger.warn('Deserialize of DataflowInformation failed with: ', err); + return { + entryPoint: data.entryPoint, + exitPoints: [], + unknownReferences: [], + in: [], + out: [], + environment: fromSerializedREnvironmentInformation({ level: data.env.level, current: new Uint8Array() }, ctx), + graph: DataflowGraph.fromSerializable(new Uint8Array()), + }; + } } /** diff --git a/src/dataflow/internal/linker.ts b/src/dataflow/internal/linker.ts index 03d93910f9d..d01c02d4555 100644 --- a/src/dataflow/internal/linker.ts +++ b/src/dataflow/internal/linker.ts @@ -44,6 +44,10 @@ export function findNonLocalReads(graph: DataflowGraph, ignore: readonly Identif const name = recoverName(id, graph.idMap); const origin = graph.getVertex(id, true); + if(id === 13){ + console.log('outgoing:', outgoing); + } + if(outgoing === undefined) { nonLocalReads.push({ name: recoverName(id, graph.idMap), @@ -58,6 +62,9 @@ export function findNonLocalReads(graph: DataflowGraph, ignore: readonly Identif if(!name) { dataflowLogger.warn('found non-local read without name for id ' + id); } + if(id === 13){ + console.log(target, ' ', ids, ' ', !ids.has(target)); + } nonLocalReads.push({ name: recoverName(id, graph.idMap), nodeId: id, @@ -344,10 +351,26 @@ export function getAllLinkedFunctionDefinitions( * @param givenInputs - The existing list of inputs that might be extended * @param graph - The graph to enter the found links * @param maybeForRemaining - Each input that can not be linked, will be added to `givenInputs`. If this flag is `true`, it will be marked as `maybe`. + * @param replaceBuiltInPlaceholders - If set, built-in read placeholders of already-linked references are replaced when the new resolution is purely user-defined. * @returns the given inputs, possibly extended with the remaining inputs (those of `referencesToLinkAgainstEnvironment` that could not be linked against the environment) */ -export function linkInputs(referencesToLinkAgainstEnvironment: readonly IdentifierReference[], environmentInformation: REnvironmentInformation, givenInputs: IdentifierReference[], graph: DataflowGraph, maybeForRemaining: boolean): IdentifierReference[] { +export function linkInputs( + referencesToLinkAgainstEnvironment: readonly IdentifierReference[], + environmentInformation: REnvironmentInformation, + givenInputs: IdentifierReference[], + graph: DataflowGraph, + maybeForRemaining: boolean, + replaceBuiltInPlaceholders = false +): IdentifierReference[] { for(const bodyInput of referencesToLinkAgainstEnvironment) { + const existingReadTargets = replaceBuiltInPlaceholders + ? [...(graph.outgoingEdges(bodyInput.nodeId)?.entries() ?? [])] + .filter(([, edge]) => edgeIncludesType(edge.types, EdgeType.Reads)) + .map(([target]) => target) + : []; + const hasOnlyBuiltInReads = existingReadTargets.length > 0 + && existingReadTargets.every(target => isBuiltIn(target)); + const probableTarget = bodyInput.name ? resolveByName(bodyInput.name, environmentInformation, bodyInput.type) : undefined; if(probableTarget === undefined) { log.trace(`found no target for ${bodyInput.name}`); @@ -364,6 +387,20 @@ export function linkInputs(referencesToLinkAgainstEnvironment: readonly Identifi allBuiltIn = false; } } + + if(replaceBuiltInPlaceholders && hasOnlyBuiltInReads && !allBuiltIn) { + const onlyUserDefinedTargets = probableTarget.every(target => + !isReferenceType(target.type, ReferenceType.BuiltInConstant | ReferenceType.BuiltInFunction) + ); + if(onlyUserDefinedTargets) { + for(const target of existingReadTargets) { + if(isBuiltIn(target)) { + graph.removeEdge(bodyInput.nodeId, target); + } + } + } + } + if(allBuiltIn) { givenInputs.push(bodyInput); } diff --git a/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts b/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts index ef29ff0848d..03c04ab331c 100644 --- a/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts +++ b/src/dataflow/internal/process/functions/call/built-in/built-in-source.ts @@ -216,15 +216,24 @@ export function processSourceCall( * Processes a source request with the given dataflow processor information and existing dataflow information * Otherwise, this can be an {@link RProjectFile} representing a standalone source file */ -export function sourceRequest(rootId: NodeId, request: RParseRequest | RProjectFile, data: DataflowProcessorInformation, information: DataflowInformation, getId?: IdGenerator): DataflowInformation { +export function sourceRequest( + rootId: NodeId, request: RParseRequest | RProjectFile, + data: DataflowProcessorInformation, + information: DataflowInformation, + getId?: IdGenerator, + allowDeferredMerge = false +): DataflowInformation { // parse, normalize and dataflow the sourced file let dataflow: DataflowInformation; let fst: RProjectFile; let filePath: string | undefined; + console.log(rootId); + if('root' in request) { fst = request; filePath = request.filePath; + console.log(request.filePath); } else { const textRequest: { r: RParseRequestFromText, path?: string } | undefined = data.ctx.files.resolveRequest(request); @@ -265,6 +274,12 @@ export function sourceRequest(rootId: NodeId, request: RParseRequest return information; } + if(allowDeferredMerge) { + return dataflow; + } + + return mergeDataflowInformation(rootId, data, filePath, information, dataflow); + // take the entry point as well as all the written references, and give them a control dependency to the source call to show that they are conditional if(!String(rootId).startsWith('file-')) { if(dataflow.graph.hasVertex(dataflow.entryPoint)) { @@ -290,6 +305,40 @@ export function sourceRequest(rootId: NodeId, request: RParseRequest }; } +/** + * + */ +export function mergeDataflowInformation(rootId: NodeId, processorInfo: DataflowProcessorInformation, + filePath: string | undefined, information: DataflowInformation, dataflow: DataflowInformation +): DataflowInformation{ + + console.log(rootId); + console.log(filePath); + // take the entry point as well as all the written references, and give them a control dependency to the source call to show that they are conditional + if(!String(rootId).startsWith('file-')) { + if(dataflow.graph.hasVertex(dataflow.entryPoint)) { + dataflow.graph.addControlDependency(dataflow.entryPoint, rootId, true); + } + for(const out of dataflow.out) { + dataflow.graph.addControlDependency(out.nodeId, rootId, true); + } + } + + processorInfo.ctx.files.addConsideredFile(filePath ?? ''); + + // update our graph with the sourced file's information + + return { + ...information, + environment: overwriteEnvironment(information.environment, dataflow.environment), + graph: information.graph.mergeWith(dataflow.graph), + in: information.in.concat(dataflow.in), + out: information.out.concat(dataflow.out), + unknownReferences: information.unknownReferences.concat(dataflow.unknownReferences), + exitPoints: dataflow.exitPoints + }; +} + /** * Processes a standalone source file (i.e., not from a source function call) */ @@ -297,7 +346,8 @@ export function standaloneSourceFile( idx: number, file: RProjectFile, data: DataflowProcessorInformation, - information: DataflowInformation + information: DataflowInformation, + allowDeferredMerge = false ): DataflowInformation { // check if the sourced file has already been dataflow analyzed, and if so, skip it if(data.referenceChain.find(e => e !== undefined && e === file.filePath)) { @@ -310,5 +360,5 @@ export function standaloneSourceFile( ...data, environment: information.environment, referenceChain: [...data.referenceChain, file.filePath] - }, information); + }, information, undefined, allowDeferredMerge); } diff --git a/src/dataflow/parallel/clonable-data.ts b/src/dataflow/parallel/clonable-data.ts new file mode 100644 index 00000000000..64eefdfef50 --- /dev/null +++ b/src/dataflow/parallel/clonable-data.ts @@ -0,0 +1,91 @@ +import type { RProject } from '../../r-bridge/lang-4.x/ast/model/nodes/r-project'; +import type { NormalizedAst, ParentInformation, RNodeWithParent } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id'; +import type { REnvironmentInformation } from '../environments/environment'; +import type { IdentifierReference } from '../environments/identifier'; +import type { DataflowGraphJson } from '../graph/graph'; +import type { ControlDependency, DataflowInformation } from '../info'; +import type { DataflowProcessorInformation } from '../processor'; + + +export interface ClonableDataflowProcessorInformation { + readonly normalizedAST: ClonableNormalizedAST + readonly environement: ClonableREnvironemntInformation + readonly referenceChain: (string | undefined)[] + readonly controlDependencies: ControlDependency[] | undefined + //readonly ctx: FlowrAnalyzerContext +} + +export interface ClonableDataflowInformation { + unknownReferences: readonly IdentifierReference[] + in: readonly IdentifierReference[] + out: readonly IdentifierReference[] + environment: ClonableREnvironemntInformation + graph: DataflowGraphJson +} + +export interface ClonableNormalizedAST>{ + idMap: Map> + ast: Node + hasError?: boolean +} + +export interface ClonableREnvironemntInformation{ + readonly current: string + readonly level: number +} + +/** + * + */ +export function toClonableDataflowProcessorInfo( + dfInfo: DataflowProcessorInformation +): ClonableDataflowProcessorInformation { + return { + normalizedAST: toClonableNormalizedAST(dfInfo.completeAst), + environement: toClonableREnvironmentInfo(dfInfo.environment), + referenceChain: dfInfo.referenceChain, + controlDependencies: dfInfo.controlDependencies, + //ctx: undefined as unknown as FlowrAnalyzerContext, + }; +} + +/** + * + */ +export function toClonableDataflowInfo( + df: DataflowInformation +): ClonableDataflowInformation { + return { + unknownReferences: df.unknownReferences, + in: df.in, + out: df.out, + environment: toClonableREnvironmentInfo(df.environment), + graph: df.graph.toJSON(), + }; +} + +/** + * + */ +export function toClonableNormalizedAST( + ast: NormalizedAst +): ClonableNormalizedAST { + return { + idMap: new Map(ast.idMap.entries()), + ast: ast.ast, + hasError: ast.hasError, + }; +} + +/** + * + */ +export function toClonableREnvironmentInfo( + env: REnvironmentInformation +): ClonableREnvironemntInformation { + return { + current: JSON.stringify(env.current), + level: env.level, + }; +} \ No newline at end of file diff --git a/src/dataflow/parallel/pool-messages.ts b/src/dataflow/parallel/pool-messages.ts new file mode 100644 index 00000000000..d03b549cefa --- /dev/null +++ b/src/dataflow/parallel/pool-messages.ts @@ -0,0 +1,159 @@ + +/** + * WorkerPool Messages + */ + +import type { TaskName } from './task-registry'; +import type { workerStats } from './worker'; +import type { MessagePort } from 'node:worker_threads'; + +export interface RegisterPortMessage { + type: 'register-port'; + workerId: number; + port: MessagePort; +} + +export interface TaskReceivedMessage { + type: 'task'; + taskName: TaskName; + taskPayload: unknown; +} + +export interface SubtaskReceivedMessage{ + type: 'subtask'; + id: number; + taskName: TaskName; + taskPayload: unknown; +} + +export interface SubtaskResponseMessage { + type: 'subtask-response'; + id: number; + result?: unknown; + error?: string; +} + +export interface PortRegisteredMessage { + type: 'port-registered'; +} + +export interface WorkerStatsRequestMessage { + type: 'worker-stats-request'; +} + +export interface WorkerFinalStatMessage { + type: 'worker-final-stats'; + workerId: number; + stats: typeof workerStats; +} + +export interface LogCorrelation { + workerId: number; + taskName?: string; + taskId?: number; + subtaskId?: number; +} + + +export type WorkerLogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export interface WorkerLogMessage { + type: 'worker-log'; + level: WorkerLogLevel; + + timestamp: number; + hrtime: [number, number]; + + message: string; + data?: unknown[]; + stack?: string; + + correlation: LogCorrelation; +} + + +/** + * + * Message Type Guards + */ + +/** + * + */ +export function isRegisterPortMessage(msg: unknown): msg is RegisterPortMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as RegisterPortMessage).type === 'register-port' && + typeof (msg as RegisterPortMessage).workerId === 'number' && + typeof (msg as RegisterPortMessage).port === 'object' + ); +} + +/** + * + */ +export function isSubtaskMessage(msg: unknown): msg is SubtaskReceivedMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as SubtaskReceivedMessage).type === 'subtask' && + typeof (msg as SubtaskReceivedMessage).id === 'number' && + typeof (msg as SubtaskReceivedMessage).taskName === 'string' + ); +} + +/** + * + */ +export function isSubtaskResponseMessage(msg: unknown): msg is SubtaskResponseMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as SubtaskResponseMessage).type === 'subtask-response' && + typeof (msg as SubtaskResponseMessage).id === 'number' + ); +} + + +/** + * + */ +export function isPortRegisteredMessage(msg: unknown): msg is PortRegisteredMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as PortRegisteredMessage).type === 'port-registered' + ); +} + +/** + * + */ +export function isWorkerLogMessage(msg: unknown): msg is WorkerLogMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as WorkerLogMessage).type === 'worker-log' + ); +} + +/** + * + */ +export function isWorkerStatsRequestMessage(msg: unknown): msg is WorkerStatsRequestMessage { + return ( + typeof msg === 'object' && + msg !== null && + (msg as WorkerStatsRequestMessage).type === 'worker-stats-request' + ); +} + +/** + * + */ +export function isWorkerFinalStatMessage(msg: unknown): msg is WorkerFinalStatMessage { + return typeof msg === 'object' && + msg !== null && + (msg as WorkerFinalStatMessage).type === 'worker-final-stats'; +} \ No newline at end of file diff --git a/src/dataflow/parallel/task-registry.ts b/src/dataflow/parallel/task-registry.ts new file mode 100644 index 00000000000..f67130c5e27 --- /dev/null +++ b/src/dataflow/parallel/task-registry.ts @@ -0,0 +1,123 @@ +import type { RProjectFile } from '../../r-bridge/lang-4.x/ast/model/nodes/r-project'; +import type { ParentInformation } from '../../r-bridge/lang-4.x/ast/model/processing/decorate'; +import type { KnownParser } from '../../r-bridge/parser'; +import { guard } from '../../util/assert'; +import type { SerializableDataflowInformation } from '../info'; +import { SerializeDataflowInformation } from '../info'; +import { DeserializeDataflowProcessorInformation, processDataflowFor, SerializeDataflowProcessorInformation, type SerializedDataflowProcessorInformation } from '../processor'; +import { processors } from '../extractor'; +import { dataflowLogger } from '../logger'; + + +export interface DataflowPayload { + index: number; + file: RProjectFile; + data: SerializedDataflowProcessorInformation; //switch with serializable version +} + +export interface DataflowReturnPayload { + processorInfo: SerializedDataflowProcessorInformation; + dataflowData: SerializableDataflowInformation; +} + +export type TaskType = 'task' | 'subtask' | 'init'; + +export type RunSubtask = ( + taskName: TaskName, + taskPayload: TInput +) => Promise; + + +let _parserEngine: KnownParser; + +/** + * + */ +export function SetParserEngine(engine: KnownParser | undefined) { + guard(engine !== undefined, 'Worker received no parser.'); + _parserEngine = engine; +} + +export const workerTasks = { + parallelFiles: ( + payload: DataflowPayload, + _runSubtask: RunSubtask + ): DataflowReturnPayload => { + // rebuild data + dataflowLogger.info('Parser Engine: ', _parserEngine); + const dataflowProcessorInfo = DeserializeDataflowProcessorInformation(payload.data, processors, _parserEngine); + + // create new DataflowInfo + //const dataflow = initializeCleanDataflowInformation(payload.file.root.info.id, dataflowProcessorInfo); + + const result = processDataflowFor( + payload.file.root, dataflowProcessorInfo + ); + + return { + processorInfo: SerializeDataflowProcessorInformation(dataflowProcessorInfo), + dataflowData: SerializeDataflowInformation(result), + }; + }, + + testPool: async ( + payload: DataflowPayload, + runSubtask: RunSubtask + ): Promise => { + console.log(`Processing ${JSON.stringify(payload.file)} @ index ${payload.index}`); + const result = await runSubtask, number>('otherFunction', {}); + const result2 = await runSubtask, number>('otherFunction', {}); + console.log(`Got ${result} and ${result2} as value from subtask`); + return undefined; + }, + + otherFunction: (): number => { + console.log('Another function as a subtask'); + return Math.random(); + }, + + __fastTask: (value: number): number => { + if(process.env.NODE_ENV !== 'test') { + throw new Error('Internal function __fastTask can only be used in test environment'); + } + return value; /** mirror value back to caller */ + }, + + __slowTask: async(value: number): Promise => { + if(process.env.NODE_ENV !== 'test') { + throw new Error('Internal function __slowTask can only be used in test environment'); + } + await new Promise(r => setTimeout(r, value)); + return value; /** mirror value back to caller */ + }, + + __spawnSubtasks: async(count: number, runSubtask: RunSubtask): Promise => { + if(process.env.NODE_ENV !== 'test') { + throw new Error('Internal function __spawnSubtasks can only be used in test environment'); + } + const tasks = []; + console.log(`Spawning ${count} subtasks`); + for(let i = 0; i < count; i++) { + tasks.push(runSubtask('__fastTask', 10)); + } + await Promise.all(tasks); + return; + }, + + __crash: () => { + if(process.env.NODE_ENV !== 'test') { + throw new Error('Internal function __crash can only be used in test environment'); + } + throw new Error('Intentional crash from __crash'); + }, + + __stall: async() => { + if(process.env.NODE_ENV !== 'test') { + throw new Error('Internal function __stall can only be used in test environment'); + } + await new Promise(() => { }); // never resolves + } +}; + +export type TaskRegistry = typeof workerTasks; +export type TaskName = keyof TaskRegistry; \ No newline at end of file diff --git a/src/dataflow/parallel/threadpool.ts b/src/dataflow/parallel/threadpool.ts new file mode 100644 index 00000000000..901e9a40b1a --- /dev/null +++ b/src/dataflow/parallel/threadpool.ts @@ -0,0 +1,447 @@ +import os from 'os'; +import type { TaskName } from './task-registry'; +import type { MessagePort } from 'node:worker_threads'; +import { Piscina } from 'piscina'; +import { dataflowLogger } from '../logger'; +import { resolve } from 'node:path'; +import { cloneConfig, defaultConfigOptions, type FlowrConfigOptions } from '../../config'; +import type { WorkerInternalState } from './worker'; +import type { SubtaskReceivedMessage, TaskReceivedMessage } from './pool-messages'; +import { isRegisterPortMessage, isSubtaskMessage, isWorkerLogMessage } from './pool-messages'; + + +type WorkerTerminationReason = 'graceful' | 'idle-timeout' | 'error' | 'forced-shutdown' | 'unknown'; + +interface WorkerLifecycle { + createdAt: number; + destroyedAt?: number; + portsOpened: number; + portClosed: number; + activeSubtasks: number; + internalState?: WorkerInternalState; + terminationReason?: WorkerTerminationReason; +} + + +export interface WorkerPoolSettings { + /** Number of workers that should be started on pool creation */ + nofMinWorkers: number; + /** Number of workers that can be alive simultaniously in the pool*/ + nofMaxWorkers: number; + /** path to the the worker file to be loaded by the pool */ + workerPath: string; + /** Timeout in milliseconds each worker can spend idle */ + idleTimeout: number; + /** Amount of tasks each worker can compute */ + concurrentTasksPerWorker: number; + /** + * Data that is given to each worker via the workerData + * Important: data needs to be clonable and data is copied for each worker + */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + workerData: { + }; +} + +export type WorkerData = WorkerPoolSettings['workerData'] & { flowrConfig: FlowrConfigOptions }; + +export const WorkerpoolDefaultSettings: WorkerPoolSettings = { + nofMinWorkers: 0, + nofMaxWorkers: 1, + workerPath: './worker.js', + idleTimeout: 30_000, // 30 seconds timeout + concurrentTasksPerWorker: 2, + workerData: {}, +}; + +export interface WorkerPoolStats { + workersCreated: number; + workersDestroyed: number; + workersDestroyedWithError: number; + workersDestroyedWithTimeout: number; + tasksExecuted: number; + tasksFailed: number; + subtasksStarted: number; + subtasksCompleted: number; + totalPortsOpened: number; + totalPortsClosed: number; +} + +export interface PoolLeakStats { + poolStats: WorkerPoolStats; + workerLifeStats: Map; +} + +export interface TaskResultWithStats { + result: T; + workerId: number; + stats: WorkerInternalState; +} + +/** + * Simple wrapper for piscina used for dataflow parallelization + */ +export class Workerpool { + private readonly pool: Piscina; + private workerPorts = new Map(); + private destroyed = false; + + private readonly workerStats: WorkerPoolStats = { + workersCreated: 0, + workersDestroyed: 0, + workersDestroyedWithError: 0, + workersDestroyedWithTimeout: 0, + tasksExecuted: 0, + tasksFailed: 0, + subtasksStarted: 0, + subtasksCompleted: 0, + totalPortsOpened: 0, + totalPortsClosed: 0, + }; + private readonly workerLifeStats = new Map(); + private readonly portCleanup = new Map void>(); + + constructor(settings: WorkerPoolSettings = WorkerpoolDefaultSettings, flowrConfig = cloneConfig(defaultConfigOptions)) { + console.log('workerPool: ', __dirname, process.env.NODE_ENV, settings.workerPath); + let workers = settings.nofMaxWorkers; + if(workers <= 0) { + // use available core + workers = Math.max(1, os.cpus().length); // may be problematic, as this returns SMT threads as cpu cores + } + const finalPath = process.env.NODE_ENV === 'test' ? + resolve('dist', 'src', 'dataflow', 'parallel', 'worker.js') : + resolve(__dirname, settings.workerPath); + + console.log(finalPath); + // create tiny pool instance + this.pool = new Piscina({ + minThreads: Math.max(settings.nofMinWorkers, 0), + maxThreads: workers, + filename: finalPath, + concurrentTasksPerWorker: settings.concurrentTasksPerWorker, + idleTimeout: settings.idleTimeout, // 30 seconds idle timeout + workerData: { + ...settings.workerData, + flowrConfig: flowrConfig, + }, + }); + + this.pool.on('message', (msg: unknown) => { + if(this.destroyed) { + console.warn('Received message after destruction of workerPool.'); + return; + } + if(!msg) { + return; + } + + if(this.tryReplayWorkerLog(msg)) { + return; + } + + // Worker sends initial port registration + if(isRegisterPortMessage(msg)) { + const { workerId, port } = msg; + + this.workerStats.totalPortsOpened++; + const lsStats = this.ensureWorkerLifeCycle(workerId); + if(lsStats) { + lsStats.portsOpened++; + } + + if(this.destroyed) { + try { + port.close(); + } catch(err: unknown) { + console.warn('Could not close port: ', err); + } + return; + } + if(this.workerPorts.has(workerId)) { + this.cleanupWorker(workerId, 're-use of worker'); + } + + const onSubtaskMessage = (subMsg: unknown) => { + if(isSubtaskMessage(subMsg)) { + void this.handleSubtask(workerId, subMsg); + } + }; + + // Listen for subtasks from this worker + port.on('message', onSubtaskMessage); + + this.portCleanup.set(workerId, () => { + port.off('message', onSubtaskMessage); + }); + + this.workerPorts.set(workerId, port); + console.log(`Port registered for ${workerId}`); + + // Confirm Registration + port.postMessage({ type: 'port-registered' }); + return; + } + }); + + this.pool.on('workerCreate', (worker: { id: number }) => { + console.log(`Worker ${worker.id} created`); + this.workerStats.workersCreated++; + this.ensureWorkerLifeCycle(worker.id); // create lifecycle if necessary + }); + + this.pool.on('workerDestroy', (worker: { id: number }) => { + this.workerStats.workersDestroyed++; + const lcStats = this.ensureWorkerLifeCycle(worker.id); + + lcStats.destroyedAt = Date.now(); + lcStats.terminationReason = this.destroyed ? 'graceful' : 'idle-timeout'; + + this.cleanupWorker(worker.id, 'worker destroyed'); + }); + + this.pool.on('error', (err) => { + this.workerStats.workersDestroyedWithError++; + console.error('Workerpool worker encountered error:', err); + }); + } + + private assertAlive() { + if(this.destroyed) { + throw new Error('Workerpool used after destruction'); + } + } + + private tryReplayWorkerLog(msg: unknown): boolean { + if(!isWorkerLogMessage(msg)) { + return false; + } + + const { + level, + message, + data, + timestamp, + correlation, + stack, + } = msg; + + const time = new Date(timestamp).toISOString(); + + const prefix = + `[${time}]` + + `[worker:${correlation.workerId}]` + + (correlation.taskId ? `[task:${correlation.taskId}]` : '') + + (correlation.subtaskId ? `[subtask:${correlation.subtaskId}]` : '') + + (correlation.taskName ? `[${correlation.taskName}]` : ''); + + console[level](prefix, message, ...(data ?? [])); + + if(stack) { + console.error(stack); + } + + return true; + } + + + private async handleSubtask(workerId: number, msg: SubtaskReceivedMessage) { + this.assertAlive(); + + this.workerStats.subtasksStarted++; + const lcStats = this.workerLifeStats.get(workerId); + if(lcStats) { + lcStats.activeSubtasks++; + } + + const { id, taskName, taskPayload } = msg; + const port = this.workerPorts.get(workerId); + console.log(`got subtask ${id} from ${workerId}`); + if(!port) { + dataflowLogger.error(`subtask submitted from worker ${workerId} has no corresponding message port. Aborting subtask`); + return; + } + + + try { + const result = await this.submitTask(taskName, taskPayload); + console.log(`resolving subtask ${taskName} @ ${id} for ${workerId}`); + port.postMessage({ type: 'subtask-response', id, result }); + } catch(err: unknown) { + port.postMessage({ + type: 'subtask-response', + id, + error: err instanceof Error ? err.message : String(err), + }); + } finally { + this.workerStats.subtasksCompleted++; + if(lcStats) { + lcStats.activeSubtasks--; + } + } + } + + async submitTask(taskName: TaskName, taskPayload: TInput): Promise { + this.assertAlive(); + this.workerStats.tasksExecuted++; + try { + /** build task payload message */ + const msg = { type: 'task', taskName, taskPayload } as TaskReceivedMessage; + /** submit and wait for result */ + const result = await (this.pool.run(msg) as Promise>); + + /** get lifecycle stats for worker */ + const lc = this.ensureWorkerLifeCycle(result.workerId); + /** update stats */ + lc.internalState = result.stats; + /** return actual task result */ + return result.result; + } catch(err: unknown) { + this.workerStats.tasksFailed++; + throw err; + } + } + + async submitTasks(taskName: TaskName, taskPayload: TInput[]): Promise { + this.assertAlive(); + // Tinypool.run returns a Promise, so we can fully parallelize: + return await Promise.all(taskPayload.map(t => this.submitTask(taskName, t))); + } + + getLeakStats(): PoolLeakStats { + return { + poolStats: this.workerStats, + workerLifeStats: this.workerLifeStats, + }; + } + + private initClosing() { + if(this.destroyed) { + return; + } + this.destroyed = true; + + for(const lc of this.workerLifeStats.values()) { + if(!lc.terminationReason && !lc.destroyedAt) { + lc.terminationReason = 'graceful'; /** normal shutdown, initiated by the user */ + } + } + //this.closeMessagePorts(); + } + + /** + * Stops all worker and rejects the promises for pending tasks + * @returns Promise, that is fullfilled when all workers are stopped + */ + async destroyPool(): Promise { + if(this.destroyed) { + return; + } + this.initClosing(); + + await this.pool.destroy(); + this.assertNoLeaks(); + } + + /** + * Stops all workers gracefully + * @param abortUnqueued - aborts non-running taks if true + * @returns Promise, that is fullfilled when all threads are finished + */ + async closePool(abortUnqueued: boolean = false): Promise { + if(this.destroyed) { + return; + } + this.initClosing(); + + await this.pool.close({ force: abortUnqueued }); + this.assertNoLeaks(); + } + + private closeMessagePorts(): void { + for(const [workerId] of this.workerPorts.entries()) { + this.cleanupWorker(workerId, 'pool is closing'); + } + if(this.workerPorts.size > 0) { + throw new Error('Failed to cleanup all worker ports'); + } + } + + private assertNoLeaks() { + for(const [workerId, lcStats] of this.workerLifeStats.entries()) { + if(!lcStats.internalState) { + console.warn(`Worker ${workerId} never transmitted internal state (${lcStats.terminationReason}).`); + } + if(lcStats.portsOpened !== lcStats.portClosed) { + console.warn(`Worker ${workerId} has leaked ports: opened ${lcStats.portsOpened}, closed ${lcStats.portClosed}`); + } + if(lcStats.activeSubtasks !== 0) { + console.warn(`Worker ${workerId} has leaked subtasks: active ${lcStats.activeSubtasks}`); + } + if(!lcStats.destroyedAt) { + console.warn(`Worker ${workerId} was not destroyed properly.`); + } + } + if(this.workerPorts.size > 0) { + console.warn('There are still worker ports open in the pool:'); + for(const workerId of this.workerPorts.keys()) { + console.warn(`Worker ${workerId} still has an open message port.`); + } + } + } + + /** + * Best effort to ensure lifecycle stats exist for a worker + * @param workerId - thread id of worker + * @returns LifeCycle stats of the worker + */ + private ensureWorkerLifeCycle(workerId: number): WorkerLifecycle { + let lc = this.workerLifeStats.get(workerId); + if(!lc) { + lc = { + createdAt: Date.now(), + portsOpened: 0, + portClosed: 0, + activeSubtasks: 0, + }; + this.workerLifeStats.set(workerId, lc); + } + return lc; + } + + private cleanupWorker(workerId: number, reason?: unknown) { + + + this.portCleanup.get(workerId)?.(); + this.portCleanup.delete(workerId); + + const port = this.workerPorts.get(workerId); + if(!port) { + return; + } // already cleaned up + + try { + port.close(); + } catch(err: unknown) { + console.warn(`Failed to close port for worker ${workerId}:`, err); + } + this.workerPorts.delete(workerId); + console.log(`Cleaned up port for worker ${workerId}`); + + this.workerStats.totalPortsClosed++; + const lcStats = this.workerLifeStats.get(workerId); + if(lcStats) { + lcStats.portClosed++; + } + + console.warn( + `Closing port for worker ${workerId}`, + { + destroyedAt: lcStats?.destroyedAt, + } + ); + console.warn( + `Worker ${workerId} cleaned up\n reason: `, + reason instanceof Error ? reason.message : reason + ); + } +} \ No newline at end of file diff --git a/src/dataflow/parallel/worker.ts b/src/dataflow/parallel/worker.ts new file mode 100644 index 00000000000..a5dd322dd11 --- /dev/null +++ b/src/dataflow/parallel/worker.ts @@ -0,0 +1,217 @@ +import { parentPort, MessageChannel, threadId, workerData } from 'node:worker_threads'; +import type { TaskName } from './task-registry'; +import { SetParserEngine, workerTasks } from './task-registry'; +import type { WorkerData } from './threadpool'; +import { dataflowLogger } from '../logger'; +import { retrieveEngineInstances } from '../../engines'; +import { cloneConfig, defaultConfigOptions } from '../../config'; +import type { TaskReceivedMessage, WorkerLogLevel } from './pool-messages'; +import { isPortRegisteredMessage, isSubtaskResponseMessage } from './pool-messages'; + + +type PendingEntry = { + resolve: (value: T | PromiseLike) => void; + reject: (reason: unknown) => void; +} + +export interface LogScope { + taskName?: string; + taskId?: number; + subtaskId?: number; +} + +export interface WorkerInternalState { + subtasksStarted: number; + subtasksCompleted: number; + pendingSubtasks: number; + shutdownReason?: string; +} + +export const workerStats: WorkerInternalState = { + subtasksStarted: 0, + subtasksCompleted: 0, + pendingSubtasks: 0, +}; + + +const pending = new Map>(); + + +const { port1: workerPort, port2: mainPort } = new MessageChannel(); +const rootLog = createLogger(); + +let portRegisteredResolve: () => void; +const portRegistered = new Promise(res => (portRegisteredResolve = res)); + +if(!parentPort) { + /** This 'should' never happen, as this port is provided natively by piscina */ + dataflowLogger.error('Worker started without parentPort present, Aborting worker'); + process.exit(1); +} + +rootLog.info(`Worker ${threadId} registering port to main thread.`); +parentPort.postMessage({ + type: 'register-port', + workerId: threadId, + port: mainPort, +}, +[mainPort] // transfer port to main thread +); + +workerPort.on('message', (msg: unknown) => { + if(destroyed){ + console.warn(`Worker ${threadId} received message after being destroyed:`, msg); + return; + } + // Listen for confirmation from main thread + if(isPortRegisteredMessage(msg)) { + portRegisteredResolve(); + return; + } + /** handle subtask responses */ + if(isSubtaskResponseMessage(msg)) { + const { id, result, error } = msg; + const logger = rootLog.child({ subtaskId: id }); + logger.info('got response for subtask request'); + const entry = pending.get(id); + if(!entry) { + return; + } + + pending.delete(id); + + if(error !== undefined) { + logger.error(`subtask failed with error: ${error}`); + entry.reject(error); + } else { + logger.info('subtask completed successfully'); + entry.resolve(result); + } + return; + } +}); + +let destroyed = false; + +function shutdown(reason: unknown) { + if(destroyed) { + return; + } + destroyed = true; + + workerStats.shutdownReason = String(reason); + workerStats.pendingSubtasks = pending.size; + + + for(const [, entry] of pending) { + entry.reject(new Error(`Worker shutdown: ${String(reason)}`)); + } + pending.clear(); + + try { + workerPort.close(); + } catch(err: unknown){ + console.warn('failed to close worker port: ', err); + } +} + +parentPort?.once('close', () => shutdown('parentPort closed')); +process.once('uncaughtException', shutdown); +process.once('unhandledRejection', shutdown); + +function createLogger(scope: LogScope = {}) { + function send( + level: WorkerLogLevel, + message: string, + data?: unknown[], + stack = false + ) { + parentPort?.postMessage({ + type: 'worker-log', + level, + timestamp: Date.now(), + hrtime: process.hrtime(), + message, + data, + stack: stack ? new Error().stack : undefined, + correlation: { + workerId: threadId, + ...scope, + }, + }); + } + + return { + debug: (msg: string, ...data: unknown[]) => + send('debug', msg, data), + info: (msg: string, ...data: unknown[]) => + send('info', msg, data), + warn: (msg: string, ...data: unknown[]) => + send('warn', msg, data), + error: (msg: string, ...data: unknown[]) => + send('error', msg, data, true), + + /** create nested correlation */ + child(extra: Partial) { + return createLogger({ ...scope, ...extra }); + }, + }; +} + +async function runSubtask(taskName: TaskName, taskPayload: TInput): Promise { + if(destroyed){ + return Promise.reject(new Error('Worker has been destroyed, cannot run subtask')); + } + const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + const logger = rootLog.child({ taskName, taskId: id }); + + workerStats.subtasksStarted++; + + return new Promise((resolve, reject) => { + pending.set(id, { + resolve: (value: unknown) => { + workerStats.subtasksCompleted++; + resolve(value as TOutput); + }, reject: (err: unknown) => { + workerStats.subtasksCompleted++; + reject(err instanceof Error ? err : new Error(String(err))); + } + }); + logger.info(`submitting subtask ${taskName} with ${id} from ${threadId}`); + // submit the subtask to main thread + workerPort.postMessage({ + type: 'subtask', + id, + taskName, + taskPayload, + }); + }); +} + +async function initialize() { + await portRegistered; + + const config = (workerData as WorkerData).flowrConfig ?? cloneConfig(defaultConfigOptions); + const engines = await retrieveEngineInstances(config, true); + SetParserEngine(engines.engines[engines.default]); + + return async(msg: TaskReceivedMessage) => { + const { taskName, taskPayload } = msg; + const logger = rootLog.child({ taskName }); + const taskHandler = workerTasks[taskName]; + if(!taskHandler) { + logger.error(`Requested unknown task (${taskName})`); + return undefined; + } + const result = await taskHandler(taskPayload as never, runSubtask); + + workerStats.pendingSubtasks = pending.size; + return { + result, + workerId: threadId, + stats: workerStats, + }; + }; +} + +module.exports = initialize(); diff --git a/src/dataflow/processor.ts b/src/dataflow/processor.ts index cf48ff98513..0d5556ec67b 100644 --- a/src/dataflow/processor.ts +++ b/src/dataflow/processor.ts @@ -2,15 +2,29 @@ * Based on a two-way fold, this processor will automatically supply scope information */ import type { ControlDependency, DataflowInformation } from './info'; -import type { - NormalizedAst, - ParentInformation, - RNodeWithParent +import { + type SerializedNormalizedAst , + DeserializeNormalizedAst, + SerializeNormalizedAst, + type NormalizedAst, + type ParentInformation, + type RNodeWithParent, } from '../r-bridge/lang-4.x/ast/model/processing/decorate'; -import type { REnvironmentInformation } from './environments/environment'; +import { fromSerializedREnvironmentInformation, toSerializedREnvironmentInformation, type REnvironmentInformation, type SerializedREnvironmentInformation } from './environments/environment'; import type { RNode } from '../r-bridge/lang-4.x/ast/model/model'; import type { KnownParserType, Parser } from '../r-bridge/parser'; -import type { FlowrAnalyzerContext } from '../project/context/flowr-analyzer-context'; +import type { SerializedFlowrAnalyzerContext } from '../project/context/flowr-analyzer-context'; +import { FlowrAnalyzerContext } from '../project/context/flowr-analyzer-context'; + +export interface SerializedDataflowProcessorInformation{ + //parser: EngineConfig['type']; + //flowrConfig: FlowrConfigOptions; + serializedAST: SerializedNormalizedAst; + environment: SerializedREnvironmentInformation; + controlDependencies: ControlDependency[] | undefined; + referenceChain: (string | undefined)[]; + ctx: SerializedFlowrAnalyzerContext; +} export interface DataflowProcessorInformation { readonly parser: Parser @@ -42,6 +56,42 @@ export interface DataflowProcessorInformation { readonly ctx: FlowrAnalyzerContext } +/** + * + */ +export function SerializeDataflowProcessorInformation( + dfInfo: DataflowProcessorInformation +): SerializedDataflowProcessorInformation { + return { + serializedAST: SerializeNormalizedAst(dfInfo.completeAst), + environment: toSerializedREnvironmentInformation(dfInfo.environment), + controlDependencies: dfInfo.controlDependencies, + referenceChain: dfInfo.referenceChain, + ctx: dfInfo.ctx.toSerializable(), + }; +} + +/** + * + */ +export function DeserializeDataflowProcessorInformation( + serializedDfInfo: SerializedDataflowProcessorInformation, + processors: DataflowProcessors, + parser: Parser +): DataflowProcessorInformation { + const ctx = FlowrAnalyzerContext.fromSerializable(serializedDfInfo.ctx); + + return { + parser, + completeAst: DeserializeNormalizedAst(serializedDfInfo.serializedAST), + environment: fromSerializedREnvironmentInformation(serializedDfInfo.environment, ctx), + processors, + referenceChain: serializedDfInfo.referenceChain, + controlDependencies: serializedDfInfo.controlDependencies, + ctx + }; +} + export type DataflowProcessor> = (node: NodeType, data: DataflowProcessorInformation) => DataflowInformation type NodeWithKey = RNode & { type: Key } diff --git a/src/documentation/wiki-dataflow-graph.ts b/src/documentation/wiki-dataflow-graph.ts index 116f19fe00c..7f4ddc98275 100644 --- a/src/documentation/wiki-dataflow-graph.ts +++ b/src/documentation/wiki-dataflow-graph.ts @@ -849,7 +849,7 @@ async function dummyDataflow(): Promise { const analyzer = await new FlowrAnalyzerBuilder().build(); analyzer.addRequest('x <- 1\nx + 1'); const result = await analyzer.dataflow(); - analyzer.close(); + await analyzer.close(); return result; } diff --git a/src/documentation/wiki-interface.ts b/src/documentation/wiki-interface.ts index 129cb1e3262..f1f2f6285d0 100644 --- a/src/documentation/wiki-interface.ts +++ b/src/documentation/wiki-interface.ts @@ -22,6 +22,7 @@ import type { DocMakerArgs } from './wiki-mk/doc-maker'; import { DocMaker } from './wiki-mk/doc-maker'; import type { KnownParser } from '../r-bridge/parser'; import type { GeneralDocContext } from './wiki-mk/doc-context'; +import { WorkerpoolDefaultSettings } from '../dataflow/parallel/threadpool'; async function explainServer(parser: KnownParser): Promise { documentAllServerMessages(); @@ -251,6 +252,14 @@ ${codeBlock('json', JSON.stringify( maxReadLines: 1_000_000 } } + }, + optimizations: { + fileParallelization: false, + dataflowOperationParallelization: false, + deferredFunctionEvaluation: false, + }, + workerPool: { + poolSettings: WorkerpoolDefaultSettings, } } satisfies FlowrConfigOptions, null, 2)) diff --git a/src/project/context/flowr-analyzer-context.ts b/src/project/context/flowr-analyzer-context.ts index ebff6ad47a4..633f742738a 100644 --- a/src/project/context/flowr-analyzer-context.ts +++ b/src/project/context/flowr-analyzer-context.ts @@ -1,9 +1,11 @@ import { + type SerializedFlowrAnalyzerFilesContext , FlowrAnalyzerFilesContext, type RAnalysisRequest, type ReadOnlyFlowrAnalyzerFilesContext } from './flowr-analyzer-files-context'; import { + type SerializedFlowrAnalyzerDependenciesContext , FlowrAnalyzerDependenciesContext, type ReadOnlyFlowrAnalyzerDependenciesContext } from './flowr-analyzer-dependencies-context'; @@ -28,6 +30,8 @@ import type { FlowrFileProvider } from './flowr-file'; import { FlowrInlineTextFile } from './flowr-file'; import type { ReadOnlyFlowrAnalyzerEnvironmentContext } from './flowr-analyzer-environment-context'; import { FlowrAnalyzerEnvironmentContext } from './flowr-analyzer-environment-context'; +import type { Workerpool } from '../../dataflow/parallel/threadpool'; +import { getPluginRegistrationName, makePlugin } from '../plugins/plugin-registry'; /** * This is a read-only interface to the {@link FlowrAnalyzerContext}. @@ -53,6 +57,27 @@ export interface ReadOnlyFlowrAnalyzerContext { readonly config: FlowrConfigOptions; } +export interface FlowrAnalyzerContextPluginNames { + filesPlugins: string[]; + discoveryPlugin: string[]; + fileLoadPlugin: string[]; + dependencyPlugin: string[]; +} + +export interface FlowrAnalyzerContextPlugins{ + filesPlugin: FlowrAnalyzerLoadingOrderPlugin[]; + discoveryPlugin: FlowrAnalyzerProjectDiscoveryPlugin[]; + fileLoadPlugin: FlowrAnalyzerFilePlugin[]; + dependencyPlugin: FlowrAnalyzerPackageVersionsPlugin[]; +} + +export interface SerializedFlowrAnalyzerContext{ + config: FlowrConfigOptions; + plugins: FlowrAnalyzerContextPluginNames; + files: SerializedFlowrAnalyzerFilesContext; + deps: SerializedFlowrAnalyzerDependenciesContext; +} + /** * This summarizes the other context layers used by the {@link FlowrAnalyzer}. * Have a look at the attributes and layers listed below (e.g., {@link files} and {@link deps}) @@ -66,19 +91,28 @@ export interface ReadOnlyFlowrAnalyzerContext { * If you are just interested in inspecting the context, you can use {@link ReadOnlyFlowrAnalyzerContext} instead (e.g., via {@link inspect}). */ export class FlowrAnalyzerContext implements ReadOnlyFlowrAnalyzerContext { - public readonly files: FlowrAnalyzerFilesContext; - public readonly deps: FlowrAnalyzerDependenciesContext; - public readonly env: FlowrAnalyzerEnvironmentContext; + public files!: FlowrAnalyzerFilesContext; + public deps!: FlowrAnalyzerDependenciesContext; + public readonly env: FlowrAnalyzerEnvironmentContext; + public readonly workerPool?: Workerpool; /** only used to pass the pool along into dataflow pipeline */ public readonly config: FlowrConfigOptions; - constructor(config: FlowrConfigOptions, plugins: ReadonlyMap) { + private readonly plugins: FlowrAnalyzerContextPlugins; + + constructor(config: FlowrConfigOptions, plugins: ReadonlyMap, workerPool?: Workerpool) { this.config = config; - const loadingOrder = new FlowrAnalyzerLoadingOrderContext(this, plugins.get(PluginType.LoadingOrder) as FlowrAnalyzerLoadingOrderPlugin[]); - this.files = new FlowrAnalyzerFilesContext(loadingOrder, (plugins.get(PluginType.ProjectDiscovery) ?? []) as FlowrAnalyzerProjectDiscoveryPlugin[], - (plugins.get(PluginType.FileLoad) ?? []) as FlowrAnalyzerFilePlugin[]); - this.deps = new FlowrAnalyzerDependenciesContext(this, (plugins.get(PluginType.DependencyIdentification) ?? []) as FlowrAnalyzerPackageVersionsPlugin[]); + this.plugins = { + filesPlugin: plugins.get(PluginType.LoadingOrder) as FlowrAnalyzerLoadingOrderPlugin[], + discoveryPlugin: (plugins.get(PluginType.ProjectDiscovery) ?? []) as FlowrAnalyzerProjectDiscoveryPlugin[], + fileLoadPlugin: (plugins.get(PluginType.FileLoad) ?? []) as FlowrAnalyzerFilePlugin[], + dependencyPlugin: (plugins.get(PluginType.DependencyIdentification) ?? []) as FlowrAnalyzerPackageVersionsPlugin[] + }; + const loadingOrder = new FlowrAnalyzerLoadingOrderContext(this, this.plugins.filesPlugin); + this.files = new FlowrAnalyzerFilesContext(loadingOrder, this.plugins.discoveryPlugin, this.plugins.fileLoadPlugin); + this.deps = new FlowrAnalyzerDependenciesContext(this, this.plugins.dependencyPlugin); this.env = new FlowrAnalyzerEnvironmentContext(this); + this.workerPool = workerPool; } /** delegate request addition */ @@ -100,6 +134,76 @@ export class FlowrAnalyzerContext implements ReadOnlyFlowrAnalyzerContext { this.deps.resolveStaticDependencies(); } + public toSerializable(): SerializedFlowrAnalyzerContext{ + // get plugin names needed for init + const pluginNames: FlowrAnalyzerContextPluginNames = { + filesPlugins: this.plugins.filesPlugin ? this.plugins.filesPlugin.map(getPluginRegistrationName) : [], + discoveryPlugin: this.plugins.discoveryPlugin !== undefined ? this.plugins.discoveryPlugin.map(getPluginRegistrationName) : [], + fileLoadPlugin: this.plugins.fileLoadPlugin !== undefined ? this.plugins.fileLoadPlugin.map(getPluginRegistrationName) : [], + dependencyPlugin: this.plugins.dependencyPlugin !== undefined ? this.plugins.dependencyPlugin.map(getPluginRegistrationName) : [], + }; + // serialize feature manager + return { + plugins: pluginNames, + config: this.config, + files: this.files.toSerializable(), + deps: this.deps.toSerializable(), + }; + } + + public static fromSerializable(analyserContext: SerializedFlowrAnalyzerContext): FlowrAnalyzerContext{ + // rebuild the plugins + const plugins = FlowrAnalyzerContext.rebuildPlugins(analyserContext.plugins); + + // rebuild the FlowrAnalyzerContext + const ctx = new FlowrAnalyzerContext(analyserContext.config, new Map([ + [PluginType.LoadingOrder, plugins.filesPlugin], + [PluginType.ProjectDiscovery, plugins.discoveryPlugin], + [PluginType.FileLoad, plugins.fileLoadPlugin], + [PluginType.DependencyIdentification, plugins.dependencyPlugin] + ])); + + // restore files and loading order + const filesCtx = FlowrAnalyzerFilesContext.fromSerializable( + analyserContext.files, + ctx, + plugins.discoveryPlugin, + plugins.fileLoadPlugin, + plugins.filesPlugin + ); + + // restore dependencies + const depsCtx = FlowrAnalyzerDependenciesContext.fromSerializable( + ctx, + analyserContext.deps, + plugins.dependencyPlugin + ); + + // bit questionable, but it works + ctx.files = filesCtx; + ctx.deps = depsCtx; + + return ctx; + + } + + private static rebuildPlugins(pluginNames: FlowrAnalyzerContextPluginNames): FlowrAnalyzerContextPlugins { + return { + filesPlugin: pluginNames.filesPlugins.map( + name => makePlugin(name) as FlowrAnalyzerLoadingOrderPlugin + ), + discoveryPlugin: pluginNames.discoveryPlugin.map( + name => makePlugin(name) as FlowrAnalyzerProjectDiscoveryPlugin + ), + fileLoadPlugin: pluginNames.fileLoadPlugin.map( + name => makePlugin(name) as FlowrAnalyzerFilePlugin + ), + dependencyPlugin: pluginNames.dependencyPlugin.map( + name => makePlugin(name) as FlowrAnalyzerPackageVersionsPlugin + ), + }; + } + /** * Get a read-only version of this context. * This is useful if you want to pass the context to a place where you do not want it to be modified or just to reduce diff --git a/src/project/context/flowr-analyzer-dependencies-context.ts b/src/project/context/flowr-analyzer-dependencies-context.ts index 1c8fcae2493..1c414568cf9 100644 --- a/src/project/context/flowr-analyzer-dependencies-context.ts +++ b/src/project/context/flowr-analyzer-dependencies-context.ts @@ -2,7 +2,8 @@ import { AbstractFlowrAnalyzerContext } from './abstract-flowr-analyzer-context' import { FlowrAnalyzerPackageVersionsPlugin } from '../plugins/package-version-plugins/flowr-analyzer-package-versions-plugin'; -import type { Package } from '../plugins/package-version-plugins/package'; +import type { SerializedPackage } from '../plugins/package-version-plugins/package'; +import { Package } from '../plugins/package-version-plugins/package'; import type { FlowrAnalyzerContext } from './flowr-analyzer-context'; /** @@ -25,6 +26,12 @@ export interface ReadOnlyFlowrAnalyzerDependenciesContext { getDependency(name: string): Package | undefined; } + +export interface SerializedFlowrAnalyzerDependenciesContext{ + dependencies: SerializedPackage[]; + staticsLoaded: boolean; +} + /** * This context is responsible for managing the dependencies of the project, including their versions and interplays with {@link FlowrAnalyzerPackageVersionsPlugin}s. * @@ -65,4 +72,28 @@ export class FlowrAnalyzerDependenciesContext extends AbstractFlowrAnalyzerConte } return this.dependencies.get(name); } + + public toSerializable(): SerializedFlowrAnalyzerDependenciesContext { + return { + dependencies: [...this.dependencies.values()].map(p => p.toSerializable()), + staticsLoaded: this.staticsLoaded, + }; + } + + public static fromSerializable( + ctx: FlowrAnalyzerContext, + data: SerializedFlowrAnalyzerDependenciesContext, + plugins?: readonly FlowrAnalyzerPackageVersionsPlugin[] + ): FlowrAnalyzerDependenciesContext { + const dependencyCtx = new FlowrAnalyzerDependenciesContext(ctx, plugins); + + for(const pkg of data.dependencies){ + dependencyCtx.addDependency(Package.fromSerializable(pkg)); + } + + dependencyCtx.staticsLoaded = data.staticsLoaded; + + return dependencyCtx; + } + } diff --git a/src/project/context/flowr-analyzer-files-context.ts b/src/project/context/flowr-analyzer-files-context.ts index 4b9804ef0a6..4c5c7555642 100644 --- a/src/project/context/flowr-analyzer-files-context.ts +++ b/src/project/context/flowr-analyzer-files-context.ts @@ -6,18 +6,23 @@ import type { import { isParseRequest } from '../../r-bridge/retriever'; import { guard } from '../../util/assert'; import type { - FlowrAnalyzerLoadingOrderContext, - ReadOnlyFlowrAnalyzerLoadingOrderContext + ReadOnlyFlowrAnalyzerLoadingOrderContext, + SerializedFlowrAnalyzerLoadingOrderContext +} from './flowr-analyzer-loading-order-context'; +import { + FlowrAnalyzerLoadingOrderContext } from './flowr-analyzer-loading-order-context'; import { FlowrAnalyzerProjectDiscoveryPlugin } from '../plugins/project-discovery/flowr-analyzer-project-discovery-plugin'; import { FlowrAnalyzerFilePlugin } from '../plugins/file-plugins/flowr-analyzer-file-plugin'; -import { type FilePath, FlowrFile, type FlowrFileProvider, FlowrTextFile, FileRole } from './flowr-file'; +import { type SerializedFlowrFile , type FilePath, FlowrFile, type FlowrFileProvider, FlowrTextFile, FileRole } from './flowr-file'; import type { FlowrDescriptionFile } from '../plugins/file-plugins/flowr-description-file'; import { log } from '../../util/log'; import fs from 'fs'; import path from 'path'; +import type { FlowrAnalyzerLoadingOrderPlugin } from '../plugins/loading-order-plugins/flowr-analyzer-loading-order-plugin'; +import type { FlowrAnalyzerContext } from './flowr-analyzer-context'; const fileLog = log.getSubLogger({ name: 'flowr-analyzer-files-context' }); @@ -104,6 +109,13 @@ export interface ReadOnlyFlowrAnalyzerFilesContext { consideredFilesList(): readonly string[]; } + +export interface SerializedFlowrAnalyzerFilesContext{ + loadingorder: SerializedFlowrAnalyzerLoadingOrderContext; + files: SerializedFlowrFile[]; + consideredFiles: string[]; +} + /** * This is the analyzer file context to be modified by all plugins that affect the files. * If you are interested in inspecting these files, refer to {@link ReadOnlyFlowrAnalyzerFilesContext}. @@ -307,4 +319,42 @@ export class FlowrAnalyzerFilesContext extends AbstractFlowrAnalyzerContext(role: Role): RoleBasedFiles[Role] { return this.byRole[role]; } + + public toSerializable(): SerializedFlowrAnalyzerFilesContext { + return { + loadingorder: this.loadingOrder.toSerilizable(), + files: [ + ...this.files.values(), + ...this.inlineFiles + ].map( f => { + guard(f instanceof FlowrFile, `Cannot serialize non-Flowr Files: ${f.constructor.name}`); + return f.toSerializable(); + }), + consideredFiles: [...this.consideredFiles], + }; + } + + public static fromSerializable( + serialized: SerializedFlowrAnalyzerFilesContext, + ctx: FlowrAnalyzerContext, + plugins: readonly FlowrAnalyzerProjectDiscoveryPlugin[], + fileLoaders: readonly FlowrAnalyzerFilePlugin[], + loadingOrderPlugin?: FlowrAnalyzerLoadingOrderPlugin[], + ): FlowrAnalyzerFilesContext { + const loadingOrder = FlowrAnalyzerLoadingOrderContext.fromSerializable(ctx, serialized.loadingorder, loadingOrderPlugin); + + const filesCtx = new FlowrAnalyzerFilesContext(loadingOrder, plugins, fileLoaders); + + // restore each file + for(const f of serialized.files){ + const file = FlowrFile.fromSerializable(f); + filesCtx.addFile(file); + } + + for(const f of serialized.consideredFiles){ + filesCtx.addConsideredFile(f); + } + + return filesCtx; + } } diff --git a/src/project/context/flowr-analyzer-loading-order-context.ts b/src/project/context/flowr-analyzer-loading-order-context.ts index 73616a81062..b8f1d4bdd78 100644 --- a/src/project/context/flowr-analyzer-loading-order-context.ts +++ b/src/project/context/flowr-analyzer-loading-order-context.ts @@ -41,6 +41,15 @@ export interface ReadOnlyFlowrAnalyzerLoadingOrderContext { const loadingOrderLog = log.getSubLogger({ name: 'loading-order' }); + +export interface SerializedFlowrAnalyzerLoadingOrderContext{ + knownOrder?: readonly RParseRequest[]; + guesses: readonly RParseRequest[][]; + unordered: readonly RParseRequest[]; + rerunRequired: boolean; +} + + /** * This context is responsible for managing the loading order of script files in a project, including guesses and known orders provided by {@link FlowrAnalyzerLoadingOrderPlugin}s. * @@ -138,4 +147,28 @@ export class FlowrAnalyzerLoadingOrderContext extends AbstractFlowrAnalyzerConte return this.peekLoadingOrder() ?? this.unordered; } + + public toSerilizable(): SerializedFlowrAnalyzerLoadingOrderContext { + return { + knownOrder: this.knownOrder, + guesses: this.guesses, + unordered: this.unordered, + rerunRequired: this.rerunRequired, + }; + } + + public static fromSerializable( + ctx: FlowrAnalyzerContext, + data: SerializedFlowrAnalyzerLoadingOrderContext, + plugins?: readonly FlowrAnalyzerLoadingOrderPlugin[] + ): FlowrAnalyzerLoadingOrderContext { + const ldOrderCtx = new FlowrAnalyzerLoadingOrderContext(ctx, plugins); + + ldOrderCtx.knownOrder = data.knownOrder; + ldOrderCtx.guesses = data.guesses.map(g => [...g]); // discard readonly + ldOrderCtx.unordered = [...data.unordered]; + ldOrderCtx.rerunRequired = data.rerunRequired; + + return ldOrderCtx; + } } \ No newline at end of file diff --git a/src/project/context/flowr-file.ts b/src/project/context/flowr-file.ts index fc05997d0e7..f7c3cf9db4d 100644 --- a/src/project/context/flowr-file.ts +++ b/src/project/context/flowr-file.ts @@ -74,6 +74,15 @@ export interface FlowrFileProvider implements private readonly cache: FlowrAnalyzerCache; private readonly ctx: FlowrAnalyzerContext; private parserInfo: KnownParserInformation | undefined; + private pool?: Workerpool; + private closed = false; /** * Create a new analyzer instance. @@ -151,6 +154,7 @@ export class FlowrAnalyzer implements this.parser = parser; this.ctx = ctx; this.cache = cache; + this.pool = ctx.workerPool; } public get flowrConfig(): FlowrConfigOptions { @@ -263,9 +267,48 @@ export class FlowrAnalyzer implements } /** - * Close the parser if it was created by this builder. This is only required if you rely on an RShell/remote engine. + * Sets the provided `feature` to `state` + * @param feature - feature to set + * @param state - boolean state */ - public close() { - return this.parser?.close(); + + /** + * Close the parser and the workerPool if it was created by this builder. This is only required if you rely on an RShell/remote engine or use parallelization. + */ + public async close(keepParserAlive = false): Promise { + if(this.closed) { + return true; + } + this.closed = true; + + // close the threadpool and wait for execution to finish + if(this.pool) { + await this.pool.closePool(); + } + if(keepParserAlive) { + return true; + } + + const result = this.parser?.close(); + return result === undefined ? undefined : result; + } + + /** + * Closes the parser and destroys the workerPool by immideatly aborting all running tasks. This should only be used in case of critical errors, where + * task results are meaningless. + */ + public async destroy(): Promise { + if(this.closed) { + return true; + } + this.closed = true; + + // destroys the threadpool and abort all tasks + if(this.pool) { + await this.pool.destroyPool(); + } + + const result = this.parser?.close(); + return result === undefined ? undefined : result; } } diff --git a/src/project/plugins/package-version-plugins/package.ts b/src/project/plugins/package-version-plugins/package.ts index 02267dffe13..bece274f729 100644 --- a/src/project/plugins/package-version-plugins/package.ts +++ b/src/project/plugins/package-version-plugins/package.ts @@ -3,6 +3,14 @@ import { guard, isNotUndefined } from '../../../util/assert'; export type PackageType = 'package' | 'system' | 'r'; +export interface SerializedPackage{ + name: string; + type?: PackageType; + derivedVersion?: string; + versionConstraints: string[]; + dependencies?: SerializedPackage[]; +} + export class Package { public name: string; public derivedVersion?: Range; @@ -61,4 +69,35 @@ export class Package { return undefined; } } + + public toSerializable(): SerializedPackage { + return { + name: this.name, + type: this.type, + derivedVersion: this.derivedVersion !== undefined ? this.derivedVersion.raw : undefined, + versionConstraints: this.versionConstraints.map(v => v.raw), + dependencies: this.dependencies !== undefined ? this.dependencies.map(d => d.toSerializable()) : undefined, + }; + } + + public static fromSerializable(serializedPackage: SerializedPackage): Package { + const pkg = new Package(serializedPackage.name, serializedPackage.type); + + if(serializedPackage.versionConstraints.length > 0) { + pkg.addInfo( + undefined, undefined, + ...serializedPackage.versionConstraints.map(v => new Range(v)) + ); + } + + if(serializedPackage.dependencies !== undefined){ + pkg.dependencies = serializedPackage.dependencies.map(d => Package.fromSerializable(d)); + } + + if(serializedPackage.derivedVersion !== undefined){ + pkg.derivedVersion = new Range(serializedPackage.derivedVersion); + } + + return pkg; + } } \ No newline at end of file diff --git a/src/project/plugins/plugin-registry.ts b/src/project/plugins/plugin-registry.ts index 141ef02c938..fd4b0f3521a 100644 --- a/src/project/plugins/plugin-registry.ts +++ b/src/project/plugins/plugin-registry.ts @@ -43,6 +43,18 @@ export function registerPluginMaker(plugin: PluginProducer, name: Exclude): FlowrAnalyzerPlugin export function getPlugin(name: string, args?: unknown[]): FlowrAnalyzerPlugin | undefined /** diff --git a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts index 35e825c14fc..6f6c59f2430 100644 --- a/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts +++ b/src/r-bridge/lang-4.x/ast/model/processing/decorate.ts @@ -11,6 +11,7 @@ import type { NoInfo, RNode } from '../model'; import { guard } from '../../../../../util/assert'; import type { SourceRange } from '../../../../../util/range'; +import type { SerializableBiMap } from '../../../../../util/collections/bimap'; import { BiMap } from '../../../../../util/collections/bimap'; import type { MergeableRecord } from '../../../../../util/objects'; import { RoleInParent } from './role'; @@ -113,8 +114,41 @@ export type RNodeWithParent = RNode = BiMap> +export type SerializableAstIdMap = SerializableBiMap> interface FoldInfo { idMap: AstIdMap, getId: IdGenerator, file?: string } + +export interface SerializedNormalizedAst>{ + idMap: SerializableAstIdMap; + ast: Node; + hasError?: boolean; +} + +/** + * + */ +export function SerializeNormalizedAst( + normalizedAst: NormalizedAst +): SerializedNormalizedAst { + return { + idMap: normalizedAst.idMap.toSerializable(), + ast: normalizedAst.ast, + hasError: normalizedAst.hasError, + }; +} + +/** + * + */ +export function DeserializeNormalizedAst(serializedAst: SerializedNormalizedAst): NormalizedAst { + return { + idMap: BiMap.fromSerializable>(serializedAst.idMap), + ast: serializedAst.ast, + hasError: serializedAst.hasError, + }; +} + + /** * Contains the normalized AST as a doubly linked tree * and a map from ids to nodes so that parent links can be chased easily. diff --git a/src/util/collections/bimap.ts b/src/util/collections/bimap.ts index d880cc0579d..ef280b5d342 100644 --- a/src/util/collections/bimap.ts +++ b/src/util/collections/bimap.ts @@ -1,3 +1,7 @@ +export interface SerializableBiMap { + entries: Array<[K, V]> +} + /** * Implementation of a bidirectional map * @@ -76,4 +80,12 @@ export class BiMap implements Map { public values(): MapIterator { return this.k2v.values(); } + + public toSerializable(): SerializableBiMap{ + return { entries: [...this.k2v] }; + } + + public static fromSerializable(data: SerializableBiMap){ + return new BiMap(data.entries); + } } diff --git a/test/functionality/_helper/leak-detection.ts b/test/functionality/_helper/leak-detection.ts new file mode 100644 index 00000000000..decd0593ece --- /dev/null +++ b/test/functionality/_helper/leak-detection.ts @@ -0,0 +1,140 @@ +import { MessagePort } from 'node:worker_threads'; + +/** Internal Node handle type */ +export type NodeHandle = unknown; + +/** Get active handles in the process (internal Node.js API) */ +export function getActiveHandles(): NodeHandle[] { + const maybeFn = (process as unknown as { _getActiveHandles?: () => unknown[] })._getActiveHandles; + if(typeof maybeFn === 'function') { + return maybeFn(); + } + return []; +} + +/** Filter only MessagePorts */ +export function getActiveMessagePorts(): MessagePort[] { + return getActiveHandles().filter((h): h is MessagePort => h instanceof MessagePort); +} + +/** Count active MessagePorts */ +export function countActiveMessagePorts(): number { + return getActiveMessagePorts().length; +} + +/** Snapshot of process handles, message ports, and memory usage */ +export interface LeakSnapshot { + handles: number; + messagePorts: number; + memoryUsage: NodeJS.MemoryUsage; +} + +/** Take a snapshot for leak detection */ +export function takeLeakSnapshot(): LeakSnapshot { + return { + handles: getActiveHandles().length, + messagePorts: countActiveMessagePorts(), + memoryUsage: process.memoryUsage(), + }; +} + +/** Diff two snapshots */ +export function diffLeakSnapshots(before: LeakSnapshot, after: LeakSnapshot): LeakSnapshot { + return { + handles: after.handles - before.handles, + messagePorts: after.messagePorts - before.messagePorts, + memoryUsage: { + rss: after.memoryUsage.rss - before.memoryUsage.rss, + heapTotal: after.memoryUsage.heapTotal - before.memoryUsage.heapTotal, + heapUsed: after.memoryUsage.heapUsed - before.memoryUsage.heapUsed, + external: after.memoryUsage.external - before.memoryUsage.external, + arrayBuffers: after.memoryUsage.arrayBuffers - before.memoryUsage.arrayBuffers, + }, + }; +} + + +export interface LeakCheckResult { + ok: boolean; + reason?: string; +} + +/** Assert no MessagePort leaks */ +export function assertNoPortLeaks(before: LeakSnapshot, after: LeakSnapshot): LeakCheckResult { + const diff = diffLeakSnapshots(before, after); + if(diff.messagePorts > 0) { + return { ok: false, reason: `Detected leaked MessagePorts (${diff.messagePorts})` }; + } + return { ok: true }; +} + +/** Assert no handle leaks */ +export function assertNoHandleLeaks(before: LeakSnapshot, after: LeakSnapshot): LeakCheckResult { + const diff = diffLeakSnapshots(before, after); + if(diff.handles > 0) { + return { ok: false, reason: `Detected leaked Handles (${diff.handles})` }; + } + return { ok: true }; +} + +/** Assert no memory leaks */ +export function assertNoMemoryLeaks( + before: LeakSnapshot, + after: LeakSnapshot, + thresholdBytes: number = 5 * 1024 * 1024 +): LeakCheckResult & { memoryDelta: number } { + const diff = diffLeakSnapshots(before, after); + if(diff.memoryUsage.heapUsed > thresholdBytes) { + return { ok: false, reason: `Heap Used increased by ${diff.memoryUsage.heapUsed} bytes`, memoryDelta: diff.memoryUsage.heapUsed }; + } + return { ok: true, memoryDelta: diff.memoryUsage.heapUsed }; +} + +/** Trigger GC if available */ +export async function forceGarbageCollection(): Promise { + if(typeof global.gc === 'function') { + global.gc(); + await new Promise(r => setImmediate(r)); + } +} + +/** Dump WTFNode handles (if installed) */ +export async function dumpWTF(reason?: string) { + try { + const wtfnode = await import('wtfnode'); // dynamic import, avoids forbidden require() + if(reason) { + console.warn(`[WTF NODE] ${reason}`); + } + wtfnode.dump({ fullStacks: true }); + } catch{ + // silently ignore if not installed + } +} + +/** Run function and measure leaks before/after */ +export async function withLeakCheck( + fn: () => Promise, + opts?: { memoryThresholdBytes?: number } +) { + await forceGarbageCollection(); + const before = takeLeakSnapshot(); + + await fn(); + + await forceGarbageCollection(); + const after = takeLeakSnapshot(); + + const memoryDiff = assertNoMemoryLeaks(before, after, opts?.memoryThresholdBytes); + + console.log(`Reported Memory Delta is ${memoryDiff.memoryDelta} bytes`); + + + return { + before, + after, + diff: diffLeakSnapshots(before, after), + portLeak: assertNoPortLeaks(before, after), + handleLeak: assertNoHandleLeaks(before, after), + memoryStable: memoryDiff, + }; +} diff --git a/test/functionality/_helper/shell.ts b/test/functionality/_helper/shell.ts index 5410846dc7d..2eddbd7d4c1 100644 --- a/test/functionality/_helper/shell.ts +++ b/test/functionality/_helper/shell.ts @@ -441,6 +441,7 @@ export function assertDataflow( console.error('diff:\n', diff); throw e; } /* v8 ignore stop */ + await analyzer.close(true); }); handleAssertOutput(name, parser, input, userConfig); } diff --git a/test/functionality/dataflow/parallel/analyzer-test-data.ts b/test/functionality/dataflow/parallel/analyzer-test-data.ts new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/test/functionality/dataflow/parallel/analyzer-test-data.ts @@ -0,0 +1 @@ + diff --git a/test/functionality/dataflow/parallel/data-serialization.test.ts b/test/functionality/dataflow/parallel/data-serialization.test.ts new file mode 100644 index 00000000000..a818a0ea0f0 --- /dev/null +++ b/test/functionality/dataflow/parallel/data-serialization.test.ts @@ -0,0 +1,180 @@ +import { assert, describe, test } from 'vitest'; +import { toSerializedREnvironmentInformation, fromSerializedREnvironmentInformation, diffEnvironments } from '../../../../src/dataflow/environments/environment'; +import { diffOfDataflowGraphs } from '../../../../src/dataflow/graph/diff-dataflow-graph'; +import { DataflowGraph } from '../../../../src/dataflow/graph/graph'; +import { VertexType } from '../../../../src/dataflow/graph/vertex'; +import type { NodeId } from '../../../../src/r-bridge/lang-4.x/ast/model/processing/node-id'; +import { FlowrAnalyzerBuilder } from '../../../../src/project/flowr-analyzer-builder'; +import type { AnalyzerSetupFunction } from './test-data/types'; +import type { TestSuite } from './test-data/test-suites'; +import { simpleDataflowTests, complexDataflowTests, sourceBasedDataflowTests } from './test-data/test-suites'; + +type EnvironmentLike = { + builtInEnv?: true; + parent?: EnvironmentLike; + memory: Map; +}; + +function compareNodeId(a: NodeId, b: NodeId): number { + return String(a).localeCompare(String(b)); +} + +function sortedNodeIds(ids: readonly NodeId[]): NodeId[] { + return [...ids].sort(compareNodeId); +} + +function assertTypeIndexConsistency(graph: DataflowGraph, messagePrefix: string): void { + const json = graph.toJSON(); + for(const tag of Object.values(VertexType)) { + const expectedIds = sortedNodeIds( + json.vertexInformation + .filter(([, vertex]) => vertex.tag === tag) + .map(([id]) => id) + ); + const actualIds = sortedNodeIds(graph.vertexIdsOfType(tag)); + assert.deepStrictEqual( + actualIds, + expectedIds, + `${messagePrefix}: internal type index mismatch for tag "${tag}"` + ); + } +} + +function assertStrictGraphSerializationEquality(original: DataflowGraph, parsed: DataflowGraph, testCaseName: string): void { + const graphdiff = diffOfDataflowGraphs( + { name: 'Original graph', graph: original }, + { name: 'Parsed graph', graph: parsed } + ); + + assert.isTrue(graphdiff.isEqual(), `Dataflow graph content differs after roundtrip for ${testCaseName}`); + + assertTypeIndexConsistency(original, `${testCaseName} (original)`); + assertTypeIndexConsistency(parsed, `${testCaseName} (parsed)`); + for(const tag of Object.values(VertexType)) { + assert.deepStrictEqual( + sortedNodeIds(parsed.vertexIdsOfType(tag)), + sortedNodeIds(original.vertexIdsOfType(tag)), + `Internal type index differs after roundtrip for ${testCaseName} in tag "${tag}"` + ); + } + + assert.deepStrictEqual( + parsed.idMap?.toSerializable(), + original.idMap?.toSerializable(), + `AST idMap differs after roundtrip for ${testCaseName}` + ); +} + +async function checkDataflowGraphSerializationStrict(testCaseName: string, setup: AnalyzerSetupFunction): Promise { + console.log(`\n► Running serialization test case: ${testCaseName}`); + + const analyzer = setup(await new FlowrAnalyzerBuilder().build()); + try { + const df = await analyzer.dataflow(); + const byteData = df.graph.toSerializable(); + const parsedGraph = DataflowGraph.fromSerializable(byteData, analyzer.context()); + + assertStrictGraphSerializationEquality(df.graph, parsedGraph, testCaseName); + } finally { + await analyzer.close(true); + } +} + +async function checkEnvironmentSerializationStrict(testCaseName: string, setup: AnalyzerSetupFunction): Promise { + console.log(`\n► Running environment serialization test case: ${testCaseName}`); + + const analyzer = setup(await new FlowrAnalyzerBuilder().build()); + try { + const df = await analyzer.dataflow(); + const serializedOriginal = toSerializedREnvironmentInformation(df.environment); + const parsedEnvironment = fromSerializedREnvironmentInformation(serializedOriginal, analyzer.context()); + const diff = diffEnvironments(df.environment.current, parsedEnvironment.current); + + assert.isTrue(diff.isEqual, `Environment differs after roundtrip for ${testCaseName}`); + assert.strictEqual(parsedEnvironment.level, df.environment.level, `Environment level differs after roundtrip for ${testCaseName}`); + assertEnvironmentLevelwiseEquality(df.environment.current as EnvironmentLike, parsedEnvironment.current as EnvironmentLike, testCaseName); + } finally { + await analyzer.close(true); + } +} + +function environmentLevels(env: EnvironmentLike): EnvironmentLike[] { + const levels: EnvironmentLike[] = []; + let current: EnvironmentLike | undefined = env; + while(current) { + levels.push(current); + if(current.builtInEnv) { + break; + } + current = current.parent; + } + return levels; +} + +function assertEnvironmentLevelwiseEquality(original: EnvironmentLike, parsed: EnvironmentLike, testCaseName: string): void { + const originalLevels = environmentLevels(original); + const parsedLevels = environmentLevels(parsed); + + assert.strictEqual( + parsedLevels.length, + originalLevels.length, + `Environment chain length differs after roundtrip for ${testCaseName}` + ); + + for(let i = 0; i < originalLevels.length; i++) { + const expectedLevel = originalLevels[i]; + const actualLevel = parsedLevels[i]; + + assert.strictEqual( + Boolean(actualLevel.builtInEnv), + Boolean(expectedLevel.builtInEnv), + `Built-in marker differs at environment level ${i} for ${testCaseName}` + ); + + const expectedKeys = [...expectedLevel.memory.keys()].sort(); + const actualKeys = [...actualLevel.memory.keys()].sort(); + assert.deepStrictEqual( + actualKeys, + expectedKeys, + `Environment memory keys differ at level ${i} for ${testCaseName}` + ); + + for(const key of expectedKeys) { + assert.deepStrictEqual( + actualLevel.memory.get(key), + expectedLevel.memory.get(key), + `Environment memory entry differs for key "${key}" at level ${i} for ${testCaseName}` + ); + } + } +} + +function registerGraphClusterTests(clusterName: string, testCluster: TestSuite): void { + for(const testCase of testCluster) { + test(`${clusterName} :: ${testCase.name}`, async() => { + await checkDataflowGraphSerializationStrict(testCase.name, testCase.setup); + }); + } +} + +function registerEnvironmentClusterTests(clusterName: string, testCluster: TestSuite): void { + for(const testCase of testCluster) { + test(`${clusterName} :: ${testCase.name}`, async() => { + await checkEnvironmentSerializationStrict(testCase.name, testCase.setup); + }); + } +} + +describe.sequential('Serialization tests', () => { + describe('Dataflow Graph Serialization', () => { + registerGraphClusterTests('Simple File Analysis', simpleDataflowTests); + registerGraphClusterTests('Complex File Analysis', complexDataflowTests); + registerGraphClusterTests('Source Based File Analysis', sourceBasedDataflowTests); + }); + + describe('Environment Serialization', () => { + registerEnvironmentClusterTests('Simple File Analysis', simpleDataflowTests); + registerEnvironmentClusterTests('Complex File Analysis', complexDataflowTests); + registerEnvironmentClusterTests('Source Based File Analysis', sourceBasedDataflowTests); + }); +}); \ No newline at end of file diff --git a/test/functionality/dataflow/parallel/parallel-dataflow.test.ts b/test/functionality/dataflow/parallel/parallel-dataflow.test.ts new file mode 100644 index 00000000000..90b9e857772 --- /dev/null +++ b/test/functionality/dataflow/parallel/parallel-dataflow.test.ts @@ -0,0 +1,132 @@ +import { assert, describe, test } from 'vitest'; +import { FlowrAnalyzerBuilder } from '../../../../src/project/flowr-analyzer-builder'; +import { diffOfDataflowGraphs } from '../../../../src/dataflow/graph/diff-dataflow-graph'; +import { graphToMermaidUrl } from '../../../../src/util/mermaid/dfg'; +import { + builtinRedefinitionOnlyTests, + cascadingSideEffectsWithRedefinitionTests, + complexDataflowTests, + fileReferenceLinkingTests, + sideEffectOnlyTests, + simpleDataflowTests, + sourceBasedDataflowTests, + standardFunctionAndClosureTests, + type NamedTestCase, + type TestSuite +} from './test-data/test-suites'; +import { someTest } from './test-data/standard-cases'; + +const knownWrongParallelCases = new Set([ + 'FunctionCallingFunction', + 'HigherOrderFunctionComposition', + 'ConditionalSideEffectAcrossFiles', + 'LoopWithSideEffect', +]); + + +async function checkGraphEquality(testCase: NamedTestCase) { + console.log(`\n► Running test case: ${testCase.name}`); + + const parallelAnalyzer = testCase.setup(await new FlowrAnalyzerBuilder() + .enableFileParallelization().build() + ); + const analyzer = testCase.setup(await new FlowrAnalyzerBuilder().build()); + + try { + const df = await parallelAnalyzer.dataflow(); + const syncDf = await analyzer.dataflow(); + + const graphdiff = diffOfDataflowGraphs( + { name: 'Parallel graph', graph: df.graph }, + { name: 'Sync graph', graph: syncDf.graph }, + testCase.expectImprecision ? { rightIsSubgraph: true } : undefined + ); + + console.log(graphdiff.comments()); + console.log(graphdiff.problematic()); + + console.log('sequential graph: ', graphToMermaidUrl(syncDf.graph, false)); + console.log('parallel graph: ', graphToMermaidUrl(df.graph, false)); + + assert.isTrue(graphdiff.isEqual(), `Dataflow graphs should be equal for testCase ${testCase.name}`); + + // Check re-analysis trigger state if expectReanalysisTrigger is defined + if(testCase.expectReanalysisTriggered !== undefined) { + console.log(`Checking re-analysis trigger state for test case ${testCase.name}...`); + console.log('reanalysisTriggered:', df.reanalysisTriggered); + console.log('reanalysisIteration:', df.reanalysisIteration); + console.log('reanalysisFileIndex:', df.reanalysisFileIndex); + assert.strictEqual( + df.reanalysisTriggered, + testCase.expectReanalysisTriggered, + `Re-analysis trigger mismatch for test case ${testCase.name}: expected ${testCase.expectReanalysisTriggered}, got ${df.reanalysisTriggered}` + ); + + // Check expected trigger file index if defined + if(testCase.expectReanalysisTriggered && testCase.expectedTriggerFileIndex !== undefined) { + assert.strictEqual( + df.reanalysisFileIndex, + testCase.expectedTriggerFileIndex, + `Trigger file index mismatch for test case ${testCase.name}: expected ${testCase.expectedTriggerFileIndex}, got ${df.reanalysisFileIndex}` + ); + } + } + } finally { + await parallelAnalyzer.close(true); + await analyzer.close(true); + } +} + +function registerClusterTests(testCluster: TestSuite) { + for(const testCase of testCluster) { + if(knownWrongParallelCases.has(testCase.name)) { + test.fails(`[KNOWN BUG] ${testCase.name}`, async() => { + await checkGraphEquality(testCase); + }); + } else { + test(`${testCase.name}`, async() => { + await checkGraphEquality(testCase); + }); + } + } +} + +describe.sequential('Parallel Dataflow test', () => { + + test('someTest', async() => { + await checkGraphEquality({ name: 'someTest', setup: someTest }); + }); + + describe('Simple File Analysis', () => { + registerClusterTests(simpleDataflowTests); + }); + + describe('Complex File Analysis', () => { + registerClusterTests(complexDataflowTests); + }); + + describe('Standard Function and Closure Analysis', () => { + registerClusterTests(standardFunctionAndClosureTests); + }); + + describe('Source Based File Analysis', () => { + registerClusterTests(sourceBasedDataflowTests); + }); + + describe('File Reference Linking', () => { + registerClusterTests(fileReferenceLinkingTests); + }); + + describe('Side Effects Only', () => { + registerClusterTests(sideEffectOnlyTests); + }); + + describe('Builtin Redefinitions Only', () => { + registerClusterTests(builtinRedefinitionOnlyTests); + }); + + describe('Cascading Side Effects With Redefinitions', () => { + registerClusterTests(cascadingSideEffectsWithRedefinitionTests); + }); + +}); \ No newline at end of file diff --git a/test/functionality/dataflow/parallel/test-data/builtin-redefinitions.ts b/test/functionality/dataflow/parallel/test-data/builtin-redefinitions.ts new file mode 100644 index 00000000000..e15eece7038 --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/builtin-redefinitions.ts @@ -0,0 +1,34 @@ +import type { AnalyzerSetupFunction } from './types'; + +/** + * Parallelization side-effect focused test data. + * + * These scenarios redefine built-in features and indicate whether a + * fallback re-analysis is expected once the redefinition is used. + */ +export const RedefinedPrintUsedAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'print <- function(...) { 7 }' }); + analyzer.addRequest({ request: 'text', content: 'x <- 1' }); + analyzer.addRequest({ request: 'text', content: 'print(x)' }); + return analyzer; +}; + +export const RedefinedPlusUsedAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: '`+` <- function(a, b) { a - b }' }); + analyzer.addRequest({ request: 'text', content: 'x <- 5' }); + analyzer.addRequest({ request: 'text', content: 'y <- x + 2' }); + return analyzer; +}; + +export const RedefinedPrintNotUsedAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'print <- function(...) { 7 }' }); + analyzer.addRequest({ request: 'text', content: 'x <- 1' }); + analyzer.addRequest({ request: 'text', content: 'y <- x + 2' }); + return analyzer; +}; + +export const BuiltinUsedWithoutRedefinitionAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 1' }); + analyzer.addRequest({ request: 'text', content: 'print(x)' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/cascading-side-effects-with-redefinitions.ts b/test/functionality/dataflow/parallel/test-data/cascading-side-effects-with-redefinitions.ts new file mode 100644 index 00000000000..a787a7cceff --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/cascading-side-effects-with-redefinitions.ts @@ -0,0 +1,35 @@ +import type { AnalyzerSetupFunction } from './types'; + +/** + * Combined cascading side-effects and builtin redefinition test data. + */ +export const CascadingSetterWithRedefinedMultiply: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: '`*` <- function(a, b) { a + b }' }); + analyzer.addRequest({ request: 'text', content: 'state <- list(a = 0, b = 0, c = 0)' }); + analyzer.addRequest({ request: 'text', content: 'setA <- function(v) { state$a <<- v }' }); + analyzer.addRequest({ request: 'text', content: 'setB <- function(v) { state$b <<- v; setA(v * 2) }' }); + analyzer.addRequest({ request: 'text', content: 'setC <- function(v) { state$c <<- v; setB(v * 3) }' }); + analyzer.addRequest({ request: 'text', content: 'setC(5)' }); + return analyzer; +}; + +export const CascadingLoggerWithRedefinedPrint: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'events <- c()' }); + analyzer.addRequest({ request: 'text', content: 'print <- function(...) { events <<- c(events, paste(...)); invisible(NULL) }' }); + analyzer.addRequest({ request: 'text', content: 'pushA <- function(x) { print("A", x) }' }); + analyzer.addRequest({ request: 'text', content: 'pushB <- function(x) { print("B", x); pushA(x + 1) }' }); + analyzer.addRequest({ request: 'text', content: 'pushC <- function(x) { print("C", x); pushB(x + 2) }' }); + analyzer.addRequest({ request: 'text', content: 'pushC(1)' }); + analyzer.addRequest({ request: 'text', content: 'result <- events' }); + return analyzer; +}; + +export const ClosureCascadeWithRedefinedPlus: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: '`+` <- function(a, b) { a * b }' }); + analyzer.addRequest({ request: 'text', content: 'acc <- 1' }); + analyzer.addRequest({ request: 'text', content: 'mk <- function(seed) { function(v) { acc <<- acc + (seed + v); acc } }' }); + analyzer.addRequest({ request: 'text', content: 'f <- mk(2)' }); + analyzer.addRequest({ request: 'text', content: 'v1 <- f(3)' }); + analyzer.addRequest({ request: 'text', content: 'v2 <- f(4)' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/file-reference-linking.ts b/test/functionality/dataflow/parallel/test-data/file-reference-linking.ts new file mode 100644 index 00000000000..2f1e0841c07 --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/file-reference-linking.ts @@ -0,0 +1,41 @@ +import { FlowrInlineTextFile } from '../../../../../src/project/context/flowr-file'; +import type { AnalyzerSetupFunction } from './types'; + +/** + * File reference linking focused test data. + * + * These tests exercise source chains and cross-file symbol usage to verify + * references are linked to the correct file-provided definitions. + */ +export const DirectSourceLinking: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('constants.R', 'PI <- 3.14')); + analyzer.addRequest({ request: 'text', content: 'source("constants.R")\nr <- 2\narea <- PI * r * r' }); + return analyzer; +}; + +export const ChainedSourceLinking: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('defs.R', 'base <- 7')); + analyzer.addFile(new FlowrInlineTextFile('bridge.R', 'source("defs.R")\nvalue <- base + 1')); + analyzer.addRequest({ request: 'text', content: 'source("bridge.R")\nresult <- value * 3' }); + return analyzer; +}; + +export const FunctionReferenceThroughSource: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('math.R', 'add <- function(a, b) { a + b }')); + analyzer.addFile(new FlowrInlineTextFile('pipeline.R', 'source("math.R")\nrun <- function(x) { add(x, 2) }')); + analyzer.addRequest({ request: 'text', content: 'source("pipeline.R")\nout <- run(5)' }); + return analyzer; +}; + +export const SourceOrderOverridesReference: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('defaults.R', 'threshold <- 5')); + analyzer.addFile(new FlowrInlineTextFile('override.R', 'threshold <- 10')); + analyzer.addRequest({ request: 'text', content: 'source("defaults.R")\nsource("override.R")\nresult <- threshold + 1' }); + return analyzer; +}; + +export const SourceInsideHelperFunction: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('helpers.R', 'helper <- function(v) { v * 4 }')); + analyzer.addRequest({ request: 'text', content: 'load <- function() { source("helpers.R") }\nload()\nresult <- helper(3)' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/function-and-closures.ts b/test/functionality/dataflow/parallel/test-data/function-and-closures.ts new file mode 100644 index 00000000000..e8e5dba4554 --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/function-and-closures.ts @@ -0,0 +1,54 @@ +import type { AnalyzerSetupFunction } from './types'; + +/** + * Standard function and closure focused test data. + * + * These cases avoid super-assignment side effects and are meant to verify + * stable lexical scoping and higher-order function behavior. + */ +export const ClosureWithCapture: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 10' }); + analyzer.addRequest({ request: 'text', content: 'makeAdder <- function(n) { function(y) { y + n } }' }); + analyzer.addRequest({ request: 'text', content: 'add5 <- makeAdder(5)' }); + analyzer.addRequest({ request: 'text', content: 'result <- add5(x)' }); + return analyzer; +}; + +export const FunctionCallingFunction: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'add <- function(a, b) { a + b }' }); + analyzer.addRequest({ request: 'text', content: 'applyTwice <- function(f, x) { f(f(x, 1), 1) }' }); + analyzer.addRequest({ request: 'text', content: 'result <- applyTwice(add, 3)' }); + return analyzer; +}; + +export const ClosureFactoryWithMultipleInstances: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'factory <- function(offset) { function(v) { v + offset } }' }); + analyzer.addRequest({ request: 'text', content: 'plus2 <- factory(2)' }); + analyzer.addRequest({ request: 'text', content: 'plus5 <- factory(5)' }); + analyzer.addRequest({ request: 'text', content: 'a <- plus2(10)\nb <- plus5(10)' }); + return analyzer; +}; + +export const NestedFunctionShadowing: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 100' }); + analyzer.addRequest({ request: 'text', content: 'outer <- function(x) { inner <- function(y) { x + y }; inner }' }); + analyzer.addRequest({ request: 'text', content: 'fn <- outer(3)' }); + analyzer.addRequest({ request: 'text', content: 'result <- fn(4)' }); + return analyzer; +}; + +export const ClosureCapturingUpdatedBinding: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'outer <- 1' }); + analyzer.addRequest({ request: 'text', content: 'f <- function() { outer }' }); + analyzer.addRequest({ request: 'text', content: 'outer <- 2' }); + analyzer.addRequest({ request: 'text', content: 'result <- f()' }); + return analyzer; +}; + +export const HigherOrderFunctionComposition: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'double <- function(x) { x * 2 }' }); + analyzer.addRequest({ request: 'text', content: 'inc <- function(x) { x + 1 }' }); + analyzer.addRequest({ request: 'text', content: 'compose <- function(f, g) { function(v) { f(g(v)) } }' }); + analyzer.addRequest({ request: 'text', content: 'f <- compose(double, inc)\nresult <- f(10)' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/side-effects.ts b/test/functionality/dataflow/parallel/test-data/side-effects.ts new file mode 100644 index 00000000000..91b119a5a4a --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/side-effects.ts @@ -0,0 +1,132 @@ +import { FlowrInlineTextFile } from '../../../../../src/project/context/flowr-file'; +import type { AnalyzerSetupFunction } from './types'; + +/** + * Side-effect focused test data + * + * These tests focus on super-assignment side effects, + * cascading writes, and side effects across file boundaries. + */ +export const ClosureWithSuperAssignment: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'counter <- 0' }); + analyzer.addRequest({ request: 'text', content: 'increment <- function() { counter <<- counter + 1; counter }' }); + analyzer.addRequest({ request: 'text', content: 'v1 <- increment()' }); + analyzer.addRequest({ request: 'text', content: 'v2 <- increment()' }); + analyzer.addRequest({ request: 'text', content: 'print(counter)' }); + return analyzer; +}; + +export const NestedClosuresWithSideEffects: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'state <- 0' }); + analyzer.addRequest({ + request: 'text', + content: 'outer <- function(x) {\n inner <- function(y) {\n state <<- state + x + y\n }\n inner\n}' + }); + analyzer.addRequest({ request: 'text', content: 'fn1 <- outer(5)' }); + analyzer.addRequest({ request: 'text', content: 'fn1(3)' }); + analyzer.addRequest({ request: 'text', content: 'fn1(2)' }); + analyzer.addRequest({ request: 'text', content: 'result <- state' }); + return analyzer; +}; + +export const CascadingSideEffects: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'globalState <- list()' }); + analyzer.addRequest({ request: 'text', content: 'setA <- function(val) { globalState$a <<- val }' }); + analyzer.addRequest({ request: 'text', content: 'setB <- function(val) { globalState$b <<- val; setA(val * 2) }' }); + analyzer.addRequest({ request: 'text', content: 'setC <- function(val) { globalState$c <<- val; setB(val * 3) }' }); + analyzer.addRequest({ request: 'text', content: 'setC(5)' }); + analyzer.addRequest({ request: 'text', content: 'print(globalState)' }); + return analyzer; +}; + +export const SourceWithClosureAndSideEffect: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('closure_lib.R', 'globalCounter <- 0\ngetCounter <- function() { globalCounter }')); + analyzer.addRequest({ request: 'text', content: 'source("closure_lib.R")' }); + analyzer.addRequest({ request: 'text', content: 'globalCounter <<- 10' }); + analyzer.addRequest({ request: 'text', content: 'val <- getCounter()' }); + return analyzer; +}; + +export const SourceChainWithClosure: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('base.R', 'baseVar <- 42\ncreateFunc <- function() { function() { baseVar } }')); + analyzer.addFile(new FlowrInlineTextFile('middle.R', 'source("base.R")\nmyFunc <- createFunc()')); + analyzer.addRequest({ request: 'text', content: 'source("middle.R")\nresult <- myFunc()\nbaseVar <<- 100\nresult2 <- myFunc()' }); + return analyzer; +}; + +export const MultipleClosuresCapturingSameVar: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'shared <- 5' }); + analyzer.addRequest({ request: 'text', content: 'fn1 <- function() { shared }' }); + analyzer.addRequest({ request: 'text', content: 'fn2 <- function() { shared }' }); + analyzer.addRequest({ request: 'text', content: 'fn3 <- function() { shared <<- shared + 1 }' }); + analyzer.addRequest({ request: 'text', content: 'fn3()' }); + analyzer.addRequest({ request: 'text', content: 'a <- fn1()\nb <- fn2()' }); + return analyzer; +}; + +export const ConditionalSideEffectAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'flag <- TRUE' }); + analyzer.addRequest({ request: 'text', content: 'globalVar <- 0' }); + analyzer.addRequest({ request: 'text', content: 'if (flag) { globalVar <<- 100 } else { globalVar <<- 200 }' }); + analyzer.addRequest({ request: 'text', content: 'result <- globalVar' }); + return analyzer; +}; + +export const LoopWithSideEffect: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'results <- c()' }); + analyzer.addRequest({ request: 'text', content: 'for (i in 1:5) { results <<- c(results, i * 2) }' }); + analyzer.addRequest({ request: 'text', content: 'print(results)' }); + return analyzer; +}; + +export const FunctionModifyingExternalState: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'db <- list(records = 0, values = c())' }); + analyzer.addRequest({ request: 'text', content: 'addRecord <- function(val) { db$records <<- db$records + 1; db$values <<- c(db$values, val) }' }); + analyzer.addRequest({ request: 'text', content: 'addRecord(10)' }); + analyzer.addRequest({ request: 'text', content: 'addRecord(20)' }); + analyzer.addRequest({ request: 'text', content: 'print(db)' }); + return analyzer; +}; + +export const RecursiveClosureWithSideEffect: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'callStack <- c()' }); + analyzer.addRequest({ + request: 'text', + content: 'factorial <- function(n) {\n callStack <<- c(callStack, n)\n if (n <= 1) 1 else n * factorial(n - 1)\n}' + }); + analyzer.addRequest({ request: 'text', content: 'result <- factorial(4)' }); + analyzer.addRequest({ request: 'text', content: 'print(callStack)' }); + return analyzer; +}; + +export const CycleDetectionWithSideEffects: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'a <- function() { globalCounter <<- globalCounter + 1; if (globalCounter < 10) b() }' }); + analyzer.addRequest({ request: 'text', content: 'b <- function() { globalCounter <<- globalCounter + 1; if (globalCounter < 10) a() }' }); + analyzer.addRequest({ request: 'text', content: 'globalCounter <- 0' }); + analyzer.addRequest({ request: 'text', content: 'a()' }); + return analyzer; +}; + +export const SourceFileWithSideEffect: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('sideEffect.R', 'loadedCount <<- if (exists("loadedCount")) loadedCount + 1 else 1')); + analyzer.addRequest({ request: 'text', content: 'loadedCount <- 0\nsource("sideEffect.R")\nsource("sideEffect.R")\nprint(loadedCount)' }); + return analyzer; +}; + +export const ClosureWithMultipleSuperAssignments: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 0\ny <- 0' }); + analyzer.addRequest({ + request: 'text', + content: 'updateBoth <- function(dx, dy) { x <<- x + dx; y <<- y + dy }' + }); + analyzer.addRequest({ request: 'text', content: 'updateBoth(5, 10)' }); + analyzer.addRequest({ request: 'text', content: 'updateBoth(3, 7)' }); + analyzer.addRequest({ request: 'text', content: 'result <- c(x, y)' }); + return analyzer; +}; + +export const SourceWithMultipleSideEffects: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('multi_se.R', 'globalA <<- 10\nglobalB <<- globalA + 5\nf <- function() { globalA + globalB }')); + analyzer.addRequest({ request: 'text', content: 'globalA <- 0\nglobalB <- 0\nsource("multi_se.R")\nresult <- f()' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/standard-cases.ts b/test/functionality/dataflow/parallel/test-data/standard-cases.ts new file mode 100644 index 00000000000..e15d6245789 --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/standard-cases.ts @@ -0,0 +1,156 @@ +import { FlowrInlineTextFile } from '../../../../../src/project/context/flowr-file'; +import type { AnalyzerSetupFunction } from './types'; + +/** + * Simple Analysis Tests Data + */ + +export const SingleFile: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 3' }); + return analyzer; +}; + +export const MultiFile: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 3' }); + analyzer.addRequest({ request: 'text', content: 'y <- 2\n print(y)' }); + analyzer.addRequest({ request: 'text', content: 'z <- x + 1\n print(z)' }); + return analyzer; +}; + +export const MultiDef: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 2' }); + analyzer.addRequest({ request: 'text', content: 'x' }); + analyzer.addRequest({ request: 'text', content: 'x <- 3' }); + analyzer.addRequest({ request: 'text', content: 'x' }); + return analyzer; +}; + +/** + * More Complex Analysis Test Data + */ + +export const ComplexVariableChains: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 5' }); + analyzer.addRequest({ request: 'text', content: 'y <- x * 2' }); + analyzer.addRequest({ request: 'text', content: 'z <- y + 1' }); + analyzer.addRequest({ request: 'text', content: 'print(z)' }); + return analyzer; +}; + +export const MultipleUsages: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'CONST <- 42' }); + analyzer.addRequest({ request: 'text', content: 'a <- CONST + 1' }); + analyzer.addRequest({ request: 'text', content: 'b <- CONST * 2' }); + analyzer.addRequest({ request: 'text', content: 'result <- a + b' }); + return analyzer; +}; + +export const FunctionDefinition: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'add <- function(a, b) { a + b }' }); + analyzer.addRequest({ request: 'text', content: 'result <- add(3, 4)' }); + analyzer.addRequest({ request: 'text', content: 'print(result)' }); + return analyzer; +}; + +export const ConditionalDefinitions: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'flag <- readline()' }); + analyzer.addRequest({ request: 'text', content: 'if (flag) { x <- 10 } else { x <- 20 }' }); + analyzer.addRequest({ request: 'text', content: 'y <- x + 5' }); + return analyzer; +}; + +export const LoopsWithCrossFile: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'dataList <- readline()' }); + analyzer.addRequest({ request: 'text', content: 'sum <- 0\nfor (val in dataList) { sum <- sum + val }' }); + analyzer.addRequest({ request: 'text', content: 'print(sum)' }); + return analyzer; +}; + +export const VariableShadowing: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 10' }); + analyzer.addRequest({ request: 'text', content: 'x <- 20' }); + analyzer.addRequest({ request: 'text', content: 'y <- x * 2' }); + return analyzer; +}; + +export const NestedFunctions: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'outer <- function(a) { inner <- function(b) { a + b } ; inner }' }); + analyzer.addRequest({ request: 'text', content: 'fn <- outer(5)' }); + analyzer.addRequest({ request: 'text', content: 'result <- fn(3)' }); + return analyzer; +}; + +export const RedefinedAssignmentOperatorAcrossFiles: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: '`<-` <- `*`' }); + analyzer.addRequest({ request: 'text', content: 'x <- 3' }); + analyzer.addRequest({ request: 'text', content: 'y = x' }); + return analyzer; +}; + +/** + * Source Based Analysis Test Data + */ + +export const SourceSimple: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('lib.R', 'helper <- function(x) { x * 2 }')); + analyzer.addRequest({ request: 'text', content: 'source("lib.R")\nresult <- helper(5)' }); + return analyzer; +}; + +export const SourceMultiple: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('config.R', 'DEBUG <- TRUE\nVERSION <- "1.0"')); + analyzer.addFile(new FlowrInlineTextFile('utils.R', 'source("config.R")\nlog <- function(msg) { if (DEBUG) print(msg) }')); + analyzer.addRequest({ request: 'text', content: 'source("utils.R")\nlog("Hello")' }); + return analyzer; +}; + +export const SourceWithDefinitions: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('module.R', 'module_var <- 42\nmodule_func <- function(x) { module_var + x }')); + analyzer.addRequest({ + request: 'text', + content: 'source("module.R")\ny <- module_var\nz <- module_func(10)\nprint(z)' + }); + return analyzer; +}; + +export const SourceChain: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('level1.R', 'x <- 1')); + analyzer.addFile(new FlowrInlineTextFile('level2.R', 'source("level1.R")\ny <- x + 1')); + analyzer.addRequest({ + request: 'text', + content: 'source("level2.R")\nz <- y * 2\nprint(z)' + }); + return analyzer; +}; + +export const SourceWithConditional: AnalyzerSetupFunction = (analyzer) => { + analyzer.addFile(new FlowrInlineTextFile('optional.R', 'optional_var <- 99')); + analyzer.addRequest({ + request: 'text', + content: 'if (TRUE) { source("optional.R") }\nresult <- optional_var + 1\nprint(result)' + }); + return analyzer; +}; + +/** + * Active Dependencies and Side Effect based tests + */ +export const ConstConditionalDefinitions: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'flag <- TRUE' }); + analyzer.addRequest({ request: 'text', content: 'if (flag) { x <- 10 } else { x <- 20 }' }); + analyzer.addRequest({ request: 'text', content: 'y <- x + 5' }); + return analyzer; +}; + +export const ConstLoopsWithCrossFile: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'data <- c(1, 2, 3)' }); + analyzer.addRequest({ request: 'text', content: 'sum <- 0\nfor (val in data) { sum <- sum + val }' }); + analyzer.addRequest({ request: 'text', content: 'print(sum)' }); + return analyzer; +}; + +export const someTest: AnalyzerSetupFunction = (analyzer) => { + analyzer.addRequest({ request: 'text', content: 'x <- 5' }); + analyzer.addRequest({ request: 'text', content: 'y <- x + 5' }); + return analyzer; +}; diff --git a/test/functionality/dataflow/parallel/test-data/test-suites.ts b/test/functionality/dataflow/parallel/test-data/test-suites.ts new file mode 100644 index 00000000000..38beb38c53a --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/test-suites.ts @@ -0,0 +1,157 @@ +import type { AnalyzerSetupFunction } from './types'; +import { + SingleFile, + MultiDef, + MultiFile, + ComplexVariableChains, + MultipleUsages, + FunctionDefinition, + ConditionalDefinitions, + LoopsWithCrossFile, + VariableShadowing, + NestedFunctions, + SourceSimple, + SourceMultiple, + SourceWithDefinitions, + SourceChain, + SourceWithConditional +} from './standard-cases'; +import { + RedefinedPrintUsedAcrossFiles, + RedefinedPlusUsedAcrossFiles, + RedefinedPrintNotUsedAcrossFiles, + BuiltinUsedWithoutRedefinitionAcrossFiles +} from './builtin-redefinitions'; +import { + ClosureWithSuperAssignment, + NestedClosuresWithSideEffects, + CascadingSideEffects, + SourceWithClosureAndSideEffect, + SourceChainWithClosure, + MultipleClosuresCapturingSameVar, + ConditionalSideEffectAcrossFiles, + LoopWithSideEffect, + FunctionModifyingExternalState, + RecursiveClosureWithSideEffect, + CycleDetectionWithSideEffects, + SourceFileWithSideEffect, + ClosureWithMultipleSuperAssignments, + SourceWithMultipleSideEffects +} from './side-effects'; +import { + ClosureWithCapture, + FunctionCallingFunction, + ClosureFactoryWithMultipleInstances, + NestedFunctionShadowing, + ClosureCapturingUpdatedBinding, + HigherOrderFunctionComposition +} from './function-and-closures'; +import { + DirectSourceLinking, + ChainedSourceLinking, + FunctionReferenceThroughSource, + SourceOrderOverridesReference, + SourceInsideHelperFunction +} from './file-reference-linking'; +import { + CascadingSetterWithRedefinedMultiply, + CascadingLoggerWithRedefinedPrint, + ClosureCascadeWithRedefinedPlus +} from './cascading-side-effects-with-redefinitions'; + +/** + * Collections of Analysis Test Cases + */ + +export type NamedTestCase = { + name: string; + setup: AnalyzerSetupFunction; + expectReanalysisTriggered?: boolean; // Optional flag to indicate if a fallback re-analysis is expected + expectedTriggerFileIndex?: number; // Optional expected file index for the trigger + expectImprecision?: boolean; // Optional flag for cases where parallel may conservatively over-approximate +}; + +export type TestSuite = NamedTestCase[]; + +export const simpleDataflowTests: TestSuite = [ + { name: 'SingleFile', setup: SingleFile }, + { name: 'MultiDef', setup: MultiDef }, + { name: 'MultiFile', setup: MultiFile } +]; + +export const complexDataflowTests: TestSuite = [ + { name: 'ComplexVariableChains', setup: ComplexVariableChains }, + { name: 'MultipleUsages', setup: MultipleUsages }, + { name: 'ConditionalDefinitions', setup: ConditionalDefinitions }, + { name: 'LoopsWithCrossFile', setup: LoopsWithCrossFile }, + { name: 'VariableShadowing', setup: VariableShadowing } +]; + +export const standardFunctionAndClosureTests: TestSuite = [ + { name: 'FunctionDefinition', setup: FunctionDefinition }, + { name: 'NestedFunctions', setup: NestedFunctions }, + { name: 'ClosureWithCapture', setup: ClosureWithCapture, expectImprecision: true }, + { name: 'FunctionCallingFunction', setup: FunctionCallingFunction }, + { name: 'ClosureFactoryWithMultipleInstances', setup: ClosureFactoryWithMultipleInstances }, + { name: 'NestedFunctionShadowing', setup: NestedFunctionShadowing }, + { name: 'ClosureCapturingUpdatedBinding', setup: ClosureCapturingUpdatedBinding, expectImprecision: true }, + { name: 'HigherOrderFunctionComposition', setup: HigherOrderFunctionComposition } +]; + +export const sourceBasedDataflowTests: TestSuite = [ + { name: 'SourceSimple', setup: SourceSimple }, + { name: 'SourceMultiple', setup: SourceMultiple }, + { name: 'SourceWithDefinitions', setup: SourceWithDefinitions }, + { name: 'SourceChain', setup: SourceChain }, + { name: 'SourceWithConditional', setup: SourceWithConditional } +]; + +export const fileReferenceLinkingTests: TestSuite = [ + { name: 'DirectSourceLinking', setup: DirectSourceLinking }, + { name: 'ChainedSourceLinking', setup: ChainedSourceLinking }, + { name: 'FunctionReferenceThroughSource', setup: FunctionReferenceThroughSource }, + { name: 'SourceOrderOverridesReference', setup: SourceOrderOverridesReference }, + { name: 'SourceInsideHelperFunction', setup: SourceInsideHelperFunction } +]; + +export const builtinRedefinitionOnlyTests: TestSuite = [ + { + name: 'RedefinedPrintUsedAcrossFiles', + setup: RedefinedPrintUsedAcrossFiles, + }, + { + name: 'RedefinedPlusUsedAcrossFiles', + setup: RedefinedPlusUsedAcrossFiles, + }, + { + name: 'RedefinedPrintNotUsedAcrossFiles', + setup: RedefinedPrintNotUsedAcrossFiles, + }, + { + name: 'BuiltinUsedWithoutRedefinitionAcrossFiles', + setup: BuiltinUsedWithoutRedefinitionAcrossFiles, + } +]; + +export const sideEffectOnlyTests: TestSuite = [ + { name: 'ClosureWithSuperAssignment', setup: ClosureWithSuperAssignment }, + { name: 'NestedClosuresWithSideEffects', setup: NestedClosuresWithSideEffects }, + { name: 'CascadingSideEffects', setup: CascadingSideEffects, expectImprecision: true }, + { name: 'SourceWithClosureAndSideEffect', setup: SourceWithClosureAndSideEffect, expectImprecision: true }, + { name: 'SourceChainWithClosure', setup: SourceChainWithClosure }, + { name: 'MultipleClosuresCapturingSameVar', setup: MultipleClosuresCapturingSameVar, expectImprecision: true }, + { name: 'ConditionalSideEffectAcrossFiles', setup: ConditionalSideEffectAcrossFiles, expectImprecision: true }, + { name: 'LoopWithSideEffect', setup: LoopWithSideEffect }, + { name: 'FunctionModifyingExternalState', setup: FunctionModifyingExternalState }, + { name: 'RecursiveClosureWithSideEffect', setup: RecursiveClosureWithSideEffect, expectImprecision: true }, + { name: 'CycleDetectionWithSideEffects', setup: CycleDetectionWithSideEffects, expectImprecision: true }, + { name: 'SourceFileWithSideEffect', setup: SourceFileWithSideEffect }, + { name: 'ClosureWithMultipleSuperAssignments', setup: ClosureWithMultipleSuperAssignments }, + { name: 'SourceWithMultipleSideEffects', setup: SourceWithMultipleSideEffects } +]; + +export const cascadingSideEffectsWithRedefinitionTests: TestSuite = [ + { name: 'CascadingSetterWithRedefinedMultiply', setup: CascadingSetterWithRedefinedMultiply }, + { name: 'CascadingLoggerWithRedefinedPrint', setup: CascadingLoggerWithRedefinedPrint }, + { name: 'ClosureCascadeWithRedefinedPlus', setup: ClosureCascadeWithRedefinedPlus } +]; diff --git a/test/functionality/dataflow/parallel/test-data/types.ts b/test/functionality/dataflow/parallel/test-data/types.ts new file mode 100644 index 00000000000..502e44824c4 --- /dev/null +++ b/test/functionality/dataflow/parallel/test-data/types.ts @@ -0,0 +1,3 @@ +import type { FlowrAnalyzer } from '../../../../../src/project/flowr-analyzer'; + +export type AnalyzerSetupFunction = (analyzer: FlowrAnalyzer) => FlowrAnalyzer; diff --git a/test/functionality/dataflow/parallel/workerpool.test.ts b/test/functionality/dataflow/parallel/workerpool.test.ts new file mode 100644 index 00000000000..9c229651625 --- /dev/null +++ b/test/functionality/dataflow/parallel/workerpool.test.ts @@ -0,0 +1,262 @@ +import { describe, expect, test } from 'vitest'; +import type { WorkerPoolSettings } from '../../../../src/dataflow/parallel/threadpool'; +import { Workerpool, WorkerpoolDefaultSettings } from '../../../../src/dataflow/parallel/threadpool'; +import { withLeakCheck } from '../../_helper/leak-detection'; + +const createPool = (overrideSettings?: Partial) => { + return new Workerpool({ + ...WorkerpoolDefaultSettings, + nofMinWorkers: 0, + nofMaxWorkers: 2, + ...overrideSettings, + }); +}; + +describe.sequential('Worker Pool Leak Tests', () => { + test('Worker and Pool Lifecycle Data Test', async() => { + const settings = { + nofMaxWorkers: 1, + }; + + const pool = createPool(settings); + + await pool.submitTask('__spawnSubtasks', 2); + + await pool.closePool(); + + const stats = pool.getLeakStats(); + + expect(stats.workerLifeStats.size).toBeGreaterThan(0); + + console.log(stats); + + const workerStats = Array.from(stats.workerLifeStats.values())[0]; + if(!workerStats) { + throw new Error('No stats for worker 0 found'); + } + + const internalState = workerStats.internalState; + console.log('Internal State:', internalState); + + + expect(workerStats.activeSubtasks).toBe(0); + expect(workerStats.terminationReason).toBe('graceful'); + + if(!internalState) { + return; + } + expect(internalState.pendingSubtasks).toBe(0); + expect(internalState.subtasksStarted).toBe(2); + expect(internalState.subtasksCompleted).toBe(2); + }); + + test('normal execution leaves no leaks', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool(); + + const res = await pool.submitTask('__fastTask', 42); + expect(res).toBe(42); + + await pool.closePool(); + + const { poolStats, workerLifeStats } = pool.getLeakStats(); + + expect(poolStats.workersCreated).toBe(poolStats.workersDestroyed); + expect(poolStats.totalPortsOpened).toBe(poolStats.totalPortsClosed); + + for(const [, lc] of workerLifeStats) { + expect(lc.activeSubtasks).toBe(0); + expect(lc.portsOpened).toBe(lc.portClosed); + expect(lc.internalState).toBeDefined(); + + if(!lc.internalState) { + continue; + } + expect(lc.internalState.pendingSubtasks).toBe(0); + expect(lc.internalState.subtasksStarted) + .toBe(lc.internalState.subtasksCompleted); + } + }); + + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + }); + + test('destroy does not wait for worker completion (best-effort)', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool(); + + const taskPromise = pool.submitTask('__slowTask', 1000); + // Attach rejection handler immediately so forced termination cannot surface as unhandled. + const taskOutcome = taskPromise.then( + () => ({ + status: 'fulfilled' as const, + }), + (error: unknown) => ({ + status: 'rejected', + error, + }) + ); + + // Immediately destroy + const destroyOutcome = pool.destroyPool().then( + () => ({ + status: 'fulfilled' as const, + }), + (error: unknown) => ({ + status: 'rejected' as const, + error, + }) + ); + + const [taskResult, destroyResult] = await Promise.all([taskOutcome, destroyOutcome]); + + expect(taskResult.status).toBe('rejected'); + if(destroyResult.status === 'rejected') { + expect(destroyResult.error).toBeInstanceOf(Error); + } + + const { workerLifeStats } = pool.getLeakStats(); + + for(const [, lc] of workerLifeStats) { + expect(lc.destroyedAt).toBeDefined(); + + if(lc.internalState) { + // IMPORTANT: these may differ + expect(lc.internalState.pendingSubtasks) + .toBeGreaterThanOrEqual(0); + + expect( + lc.internalState.subtasksStarted >= + lc.internalState.subtasksCompleted + ).toBe(true); + } + } + }); + + // Even forced shutdown must not leak ports + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + }); + + test('idle timeout shutdown preserves best-effort stats', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool({ + idleTimeout: 10, + }); + + await pool.submitTask('__fastTask', 1); + await new Promise(r => setTimeout(r, 50)); + + await pool.closePool(); + + const { workerLifeStats } = pool.getLeakStats(); + + console.log(workerLifeStats); + + for(const [, lc] of workerLifeStats) { + expect(lc.terminationReason).toBe('idle-timeout'); + expect(lc.internalState).toBeDefined(); + } + }); + + expect(result.portLeak.ok).toBe(true); + }); + + test('repeated pool creation does not leak MessagePorts', async() => { + const result = await withLeakCheck(async() => { + for(let i = 0; i < 10; i++) { + const pool = createPool(); + await pool.submitTask('__fastTask', i); + await pool.closePool(); + } + }); + + expect(result.portLeak.ok).toBe(true); + expect(result.diff.messagePorts).toBe(0); + }); + + test('subtask-heavy workload leaves no leaks', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool(); + + await pool.submitTask('__spawnSubtasks', { + count: 100, + }); + + await pool.closePool(); + }, { + memoryThresholdBytes: 10 * 1024 * 1024, + }); + + expect(result.portLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + }); + + +}); + + +describe.sequential('Worker Pool Heavy Load and Leak Tests', () => { + test('massive parallel subtask execution does not leak ports or memory', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool({ nofMaxWorkers: 4 }); + + await pool.submitTask('__spawnSubtasks', 200); + + await pool.closePool(); + }, { memoryThresholdBytes: 20 * 1024 * 1024 }); + + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + }); + + test('continuous task submission over multiple pools', async() => { + const result = await withLeakCheck(async() => { + for(let i = 0; i < 5; i++) { + const pool = createPool({ nofMaxWorkers: 2 }); + await pool.submitTask('__spawnSubtasks', 50); + await pool.closePool(); + } + }, { memoryThresholdBytes: 15 * 1024 * 1024 }); + + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + expect(result.diff.messagePorts).toBe(0); + }); + + test('subtask-heavy workload leaves no leaks', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool({ nofMaxWorkers: 3 }); + + await pool.submitTask('__spawnSubtasks', 500); + + await pool.closePool(); + }, { memoryThresholdBytes: 25 * 1024 * 1024 }); + + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + }); + + test('mix of fast and slow tasks under heavy load', async() => { + const result = await withLeakCheck(async() => { + const pool = createPool({ nofMaxWorkers: 3 }); + + const fastTasks = Array.from({ length: 50 }, () => pool.submitTask('__fastTask', 1)); + const slowTasks = Array.from({ length: 10 }, () => pool.submitTask('__slowTask', 50)); + + await Promise.all([...fastTasks, ...slowTasks]); + + await pool.closePool(); + }, { memoryThresholdBytes: 15 * 1024 * 1024 }); + + expect(result.portLeak.ok).toBe(true); + expect(result.handleLeak.ok).toBe(true); + expect(result.memoryStable.ok).toBe(true); + }); + +}); \ No newline at end of file