diff --git a/.editorconfig b/.editorconfig index 1a9b35ea45..fc32fae553 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true end_of_line = lf insert_final_newline = true charset = utf-8 -indent_size = 4 trim_trailing_whitespace = true [*.{cjs,js,jsx,mjs,ts,tsx}] diff --git a/.github/path-filters.yml b/.github/path-filters.yml index d980a41921..d311e84e3f 100644 --- a/.github/path-filters.yml +++ b/.github/path-filters.yml @@ -11,6 +11,9 @@ any-workspace: - *global - "packages/**" +task-herder: + - *global + - "packages/task-herder/**" scratch-svg-renderer: - *global - "packages/scratch-svg-renderer/**" diff --git a/package-lock.json b/package-lock.json index eae99e28d2..671883a4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "12.0.2-hotfix", "license": "AGPL-3.0-only", "workspaces": [ + "packages/task-herder", "packages/scratch-svg-renderer", "packages/scratch-render", "packages/scratch-vm", @@ -18,6 +19,7 @@ "@commitlint/cli": "17.8.1", "@commitlint/config-conventional": "17.8.1", "cross-env": "7.0.3", + "globals": "16.5.0", "husky": "8.0.3", "npm": "10.9.4", "ts-node": "10.9.2" @@ -156,7 +158,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2464,7 +2465,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2487,7 +2487,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2502,6 +2501,40 @@ "node": ">=10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.76.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.76.0.tgz", @@ -4439,8 +4472,7 @@ "version": "0.4.1646425229", "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@microbit/microbit-universal-hex": { "version": "0.2.2", @@ -4456,6 +4488,19 @@ "tslib": ">=1.11.1" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", @@ -4943,7 +4988,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -5095,6 +5139,26 @@ "@octokit/openapi-types": "^18.0.0" } }, + "node_modules/@oxc-project/runtime": { + "version": "0.92.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz", + "integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.93.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.93.0.tgz", + "integrity": "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5683,6 +5747,294 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.41.tgz", + "integrity": "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.41.tgz", + "integrity": "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.41.tgz", + "integrity": "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.41.tgz", + "integrity": "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.41.tgz", + "integrity": "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.41.tgz", + "integrity": "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.41.tgz", + "integrity": "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.41.tgz", + "integrity": "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.41.tgz", + "integrity": "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -5715,6 +6067,10 @@ "resolved": "packages/scratch-vm", "link": true }, + "node_modules/@scratch/task-herder": { + "resolved": "packages/task-herder", + "link": true + }, "node_modules/@semantic-release/commit-analyzer": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-9.0.2.tgz", @@ -8840,6 +9196,13 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.5.0.tgz", @@ -9018,7 +9381,6 @@ "integrity": "sha512-NBR5lf/M1hrC+YCNszJQWQzXnnN3Rj76qpNcpXFqZ73ZZpgyZHZ0qjWWWSA5D6OZmYpHEDXn3+LZ1p3b93xW8Q==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@tapjs/processinfo": "^3.1.8", "@tapjs/stack": "4.0.2", @@ -9937,7 +10299,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@tensorflow/tfjs-backend-cpu": "4.22.0", "@types/offscreencanvas": "~2019.3.0", @@ -9956,7 +10317,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } @@ -9966,7 +10326,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/long": "^4.0.1", "@types/offscreencanvas": "~2019.7.0", @@ -10338,6 +10697,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -10406,6 +10776,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -10437,6 +10818,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -10691,7 +11079,6 @@ "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -10740,7 +11127,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -10825,7 +11211,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -10837,7 +11222,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -11050,7 +11434,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -11282,6 +11665,214 @@ "integrity": "sha512-poS0LZ3jAjH36gIAI0aNBBdsGGbmt11VFbLO+eGDJ/JDSPtMu1iUStvOi0UM/ZH6Jyh34SjVd8Cnxu/Wmcb8iQ==", "license": "BSD-3-Clause" }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.7.tgz", + "integrity": "sha512-MXc+kEA5EUwMMGmNt1S6CIOEl/iCmAhGZQq1QgMNC3/QpYSOxkysEi6pxWhkqJ7YT/RduoVEV5rxFxHG18V3LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.7", + "ast-v8-to-istanbul": "^0.3.5", + "debug": "^4.4.3", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.7", + "vitest": "4.0.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.7.tgz", + "integrity": "sha512-jGRG6HghnJDjljdjYIoVzX17S6uCVCBRFnsgdLGJ6CaxfPh8kzUKe/2n533y4O/aeZ/sIr7q7GbuEbeGDsWv4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.7", + "@vitest/utils": "4.0.7", + "chai": "^6.0.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.7.tgz", + "integrity": "sha512-OsDwLS7WnpuNslOV6bJkXVYVV/6RSc4eeVxV7h9wxQPNxnjRvTTrIikfwCbMyl8XJmW6oOccBj2Q07YwZtQcCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.19" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.7.tgz", + "integrity": "sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.7.tgz", + "integrity": "sha512-orU1lsu4PxLEcDWfjVCNGIedOSF/YtZ+XMrd1PZb90E68khWCNzD8y1dtxtgd0hyBIQk8XggteKN/38VQLvzuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.7.tgz", + "integrity": "sha512-xJL+Nkw0OjaUXXQf13B8iKK5pI9QVtN9uOtzNHYuG/o/B7fIEg0DQ+xOe0/RcqwDEI15rud1k7y5xznBKGUXAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.7", + "magic-string": "^0.30.19", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.7.tgz", + "integrity": "sha512-FW4X8hzIEn4z+HublB4hBF/FhCVaXfIHm8sUfvlznrcy1MQG7VooBgZPMtVCGZtHi0yl3KESaXTqsKh16d8cFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.7.tgz", + "integrity": "sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.7", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -11540,7 +12131,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11654,7 +12244,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11777,6 +12366,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -12050,7 +12649,6 @@ "resolved": "https://registry.npmjs.org/arraybuffer-loader/-/arraybuffer-loader-1.0.8.tgz", "integrity": "sha512-CwUVCcxCgcgZUu2w741OV6Xj1tvRVQebq22RCyGXiLgJOJ4e4M/59EPYdtK2MLfIN28t1TDvuh2ojstNq3Kh5g==", "license": "MIT", - "peer": true, "dependencies": { "loader-utils": "^1.1.0" }, @@ -12108,6 +12706,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -12115,6 +12723,36 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -12343,7 +12981,6 @@ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" @@ -12960,7 +13597,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -13610,6 +14246,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14174,6 +14820,13 @@ "dot-prop": "^5.1.0" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -14296,6 +14949,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -14498,7 +15158,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14696,7 +15355,6 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -14903,7 +15561,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15740,6 +16397,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -16693,7 +17360,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -16998,7 +17664,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -17383,6 +18048,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -17484,6 +18159,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -17577,7 +18262,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17730,6 +18414,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -18023,7 +18714,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22584,7 +23274,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -24674,6 +25363,13 @@ "node": ">= 8" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -24738,6 +25434,267 @@ "immediate": "~3.0.5" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/linebreak": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-0.3.0.tgz", @@ -24863,6 +25820,24 @@ "json5": "lib/cli.js" } }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -25126,6 +26101,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -25523,7 +26510,6 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -26850,6 +27836,38 @@ "integrity": "sha512-PbNHr7Y/9Y/2P5pKFv5XOGBfNQqZ+fdiHWcuf7swLACN5ZW5LU7J5tMU8LSBjpluAxAxKYGD9nnaIbdRy9+m1w==", "license": "MIT" }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/mocha": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", @@ -30139,7 +31157,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -31310,6 +32327,13 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -31397,6 +32421,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -31658,6 +32689,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pkg-types": { + "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.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/playwright-chromium": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.56.1.tgz", @@ -31746,7 +32789,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -31829,7 +32871,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -32049,7 +33090,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -32270,7 +33310,6 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -32383,6 +33422,23 @@ "node": ">=0.6" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/query-string": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", @@ -32577,7 +33633,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -32667,7 +33722,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -32922,7 +33976,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -33015,7 +34068,6 @@ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", "license": "MIT", - "peer": true, "dependencies": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", @@ -33048,7 +34100,6 @@ "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", "license": "MIT", - "peer": true, "dependencies": { "prop-types": "^15.5.4" } @@ -33724,7 +34775,6 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", - "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -34060,6 +35110,40 @@ "rimraf": "bin.js" } }, + "node_modules/rolldown": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz", + "integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.93.0", + "@rolldown/pluginutils": "1.0.0-beta.41", + "ansis": "=4.2.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.41", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.41", + "@rolldown/binding-darwin-x64": "1.0.0-beta.41", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.41", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.41", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.41", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.41", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.41", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.41", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.41", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.41", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.41", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.41", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.41" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -34467,7 +35551,6 @@ "version": "1.0.250", "resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.250.tgz", "integrity": "sha512-B8IW5C75u0BBuptKpqujS7/l4fi3d+0kTmMynf6ucu1BeT84ib8lgso1Ib1l1o2xYmF8h9m4v+vCmtRLOvg4YA==", - "peer": true, "dependencies": { "base64-loader": "^1.0.0" } @@ -34567,8 +35650,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/seek-bzip": { "version": "1.0.6", @@ -34632,7 +35714,6 @@ "integrity": "sha512-NMPKdfpXTnPn49FDogMBi36SiBfXkSOJqCkk0E4iWOY1tusvvgBwqUmxTX1kmlT6kIYed9YwNKD1sfPpqa5yaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^9.0.2", "@semantic-release/error": "^3.0.0", @@ -35207,6 +36288,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -35672,6 +36760,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/startaudiocontext": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/startaudiocontext/-/startaudiocontext-1.2.1.tgz", @@ -35908,6 +37003,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -36286,7 +37388,6 @@ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 18.12.0" }, @@ -37058,6 +38159,20 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -37099,7 +38214,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -37107,6 +38221,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -37349,7 +38473,6 @@ "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", @@ -37439,7 +38562,6 @@ "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -37499,7 +38621,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -37733,8 +38854,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.6", @@ -38147,7 +39267,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -38187,6 +39306,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -38464,6 +39590,89 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-dts": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.0.0-beta.6.tgz", + "integrity": "sha512-+xbFv5aVFtLZFNBAKI4+kXmd2h+T42/AaP8Bsp0YP/je/uOTN94Ame2Xt3e9isZS+Z7/hrLCLbsVJh+saqFMfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "@volar/typescript": "^2.4.17", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "unplugin": "^2.3.2" + }, + "peerDependencies": { + "@microsoft/api-extractor": ">=7", + "@rspack/core": "^1", + "@vue/language-core": "~3.0.1", + "esbuild": "*", + "rolldown": "*", + "rollup": ">=3", + "typescript": ">=4", + "vite": ">=3", + "webpack": "^4 || ^5" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "@vue/language-core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -38516,7 +39725,6 @@ "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -38545,7 +39753,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -38817,12 +40024,218 @@ "extsprintf": "^1.2.0" } }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", + "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.92.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.41", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.25.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.7.tgz", + "integrity": "sha512-xQroKAadK503CrmbzCISvQUjeuvEZzv6U0wlnlVFOi5i3gnzfH4onyQ29f3lzpe0FresAiTAd3aqK0Bi/jLI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.7", + "@vitest/mocker": "4.0.7", + "@vitest/pretty-format": "4.0.7", + "@vitest/runner": "4.0.7", + "@vitest/snapshot": "4.0.7", + "@vitest/spy": "4.0.7", + "@vitest/utils": "4.0.7", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.19", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.7", + "@vitest/browser-preview": "4.0.7", + "@vitest/browser-webdriverio": "4.0.7", + "@vitest/ui": "4.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", "license": "MIT" }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -38929,7 +40342,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -38979,7 +40391,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -39208,6 +40619,13 @@ "source-map": "~0.6.1" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -39427,6 +40845,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -39732,7 +41167,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -39841,7 +41275,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -40385,7 +41818,6 @@ "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -40427,6 +41859,230 @@ "optional": true } } + }, + "packages/task-herder": { + "name": "@scratch/task-herder", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "dependencies": { + "p-limit": "7.2.0" + }, + "devDependencies": { + "@vitest/coverage-v8": "4.0.7", + "eslint": "9.39.1", + "eslint-config-scratch": "12.0.24", + "prettier": "3.6.2", + "typescript": "~5.9.3", + "unplugin-dts": "1.0.0-beta.6", + "vite": "npm:rolldown-vite@7.1.14", + "vitest": "4.0.7" + } + }, + "packages/task-herder/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/task-herder/node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "packages/task-herder/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/task-herder/node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "packages/task-herder/node_modules/eslint-config-scratch": { + "version": "12.0.24", + "resolved": "https://registry.npmjs.org/eslint-config-scratch/-/eslint-config-scratch-12.0.24.tgz", + "integrity": "sha512-IFhsFSJfdp3UobAvN66T3DMMfAMCwOSmlvbuk3a+PpgDlXIGJVmwdMRUC/3AQWgoMDXQVWLmfwbC6V/xiDghQw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/eslint-parser": "7.28.5", + "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", + "@eslint/eslintrc": "3.3.1", + "@eslint/js": "9.39.0", + "@eslint/markdown": "7.5.0", + "@stylistic/eslint-plugin": "^5.3.1", + "@trivago/prettier-plugin-sort-imports": "5.2.2", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-formatjs": "5.4.2", + "eslint-plugin-html": "8.1.3", + "eslint-plugin-import": "2.32.0", + "eslint-plugin-jsdoc": "61.1.11", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.0.1", + "globals": "16.4.0", + "prettier": "3.6.2", + "typescript-eslint": "8.46.2" + }, + "peerDependencies": { + "eslint": "^9.23.0" + } + }, + "packages/task-herder/node_modules/eslint-config-scratch/node_modules/@eslint/js": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "packages/task-herder/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "packages/task-herder/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/task-herder/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "packages/task-herder/node_modules/p-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz", + "integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/task-herder/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index ae139af97c..0a08074bff 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ } }, "workspaces": [ + "packages/task-herder", "packages/scratch-svg-renderer", "packages/scratch-render", "packages/scratch-vm", @@ -34,6 +35,7 @@ "@commitlint/cli": "17.8.1", "@commitlint/config-conventional": "17.8.1", "cross-env": "7.0.3", + "globals": "16.5.0", "husky": "8.0.3", "npm": "10.9.4", "ts-node": "10.9.2" diff --git a/packages/scratch-vm/src/util/task-queue.js b/packages/scratch-vm/src/util/task-queue.js deleted file mode 100644 index d71bc1d972..0000000000 --- a/packages/scratch-vm/src/util/task-queue.js +++ /dev/null @@ -1,194 +0,0 @@ -const Timer = require('../util/timer'); - -/** - * This class uses the token bucket algorithm to control a queue of tasks. - */ -class TaskQueue { - /** - * Creates an instance of TaskQueue. - * To allow bursts, set `maxTokens` to several times the average task cost. - * To prevent bursts, set `maxTokens` to the cost of the largest tasks. - * Note that tasks with a cost greater than `maxTokens` will be rejected. - * @param {number} maxTokens - the maximum number of tokens in the bucket (burst size). - * @param {number} refillRate - the number of tokens to be added per second (sustain rate). - * @param {object} options - optional settings for the new task queue instance. - * @property {number} startingTokens - the number of tokens the bucket starts with (default: `maxTokens`). - * @property {number} maxTotalCost - reject a task if total queue cost would pass this limit (default: no limit). - * @memberof TaskQueue - */ - constructor (maxTokens, refillRate, options = {}) { - this._maxTokens = maxTokens; - this._refillRate = refillRate; - this._pendingTaskRecords = []; - this._tokenCount = Object.prototype.hasOwnProperty.call(options, 'startingTokens') ? - options.startingTokens : maxTokens; - this._maxTotalCost = Object.prototype.hasOwnProperty.call(options, 'maxTotalCost') ? - options.maxTotalCost : Infinity; - this._timer = new Timer(); - this._timer.start(); - this._timeout = null; - this._lastUpdateTime = this._timer.timeElapsed(); - - this._runTasks = this._runTasks.bind(this); - } - - /** - * Get the number of queued tasks which have not yet started. - * @readonly - * @memberof TaskQueue - */ - get length () { - return this._pendingTaskRecords.length; - } - - /** - * Wait until the token bucket is full enough, then run the provided task. - * @param {Function} task - the task to run. - * @param {number} [cost] - the number of tokens this task consumes from the bucket. - * @returns {Promise} - a promise for the task's return value. - * @memberof TaskQueue - */ - do (task, cost = 1) { - if (this._maxTotalCost < Infinity) { - const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0); - if (currentTotalCost + cost > this._maxTotalCost) { - return Promise.reject(new Error('Maximum total cost exceeded')); - } - } - const newRecord = { - cost - }; - newRecord.promise = new Promise((resolve, reject) => { - newRecord.cancel = () => { - reject(new Error('Task canceled')); - }; - - // The caller, `_runTasks()`, is responsible for cost-checking and spending tokens. - newRecord.wrappedTask = () => { - try { - resolve(task()); - } catch (e) { - reject(e); - } - }; - }); - this._pendingTaskRecords.push(newRecord); - - // If the queue has been idle we need to prime the pump - if (this._pendingTaskRecords.length === 1) { - this._runTasks(); - } - - return newRecord.promise; - } - - /** - * Cancel one pending task, rejecting its promise. - * @param {Promise} taskPromise - the promise returned by `do()`. - * @returns {boolean} - true if the task was found, or false otherwise. - * @memberof TaskQueue - */ - cancel (taskPromise) { - const taskIndex = this._pendingTaskRecords.findIndex(r => r.promise === taskPromise); - if (taskIndex !== -1) { - const [taskRecord] = this._pendingTaskRecords.splice(taskIndex, 1); - taskRecord.cancel(); - if (taskIndex === 0 && this._pendingTaskRecords.length > 0) { - this._runTasks(); - } - return true; - } - return false; - } - - /** - * Cancel all pending tasks, rejecting all their promises. - * @memberof TaskQueue - */ - cancelAll () { - if (this._timeout !== null) { - this._timer.clearTimeout(this._timeout); - this._timeout = null; - } - const oldTasks = this._pendingTaskRecords; - this._pendingTaskRecords = []; - oldTasks.forEach(r => r.cancel()); - } - - /** - * Shorthand for calling _refill() then _spend(cost). - * @see {@link TaskQueue#_refill} - * @see {@link TaskQueue#_spend} - * @param {number} cost - the number of tokens to try to spend. - * @returns {boolean} true if we had enough tokens; false otherwise. - * @memberof TaskQueue - */ - _refillAndSpend (cost) { - this._refill(); - return this._spend(cost); - } - - /** - * Refill the token bucket based on the amount of time since the last refill. - * @memberof TaskQueue - */ - _refill () { - const now = this._timer.timeElapsed(); - const timeSinceRefill = now - this._lastUpdateTime; - if (timeSinceRefill <= 0) return; - - this._lastUpdateTime = now; - this._tokenCount += timeSinceRefill * this._refillRate / 1000; - this._tokenCount = Math.min(this._tokenCount, this._maxTokens); - } - - /** - * If we can "afford" the given cost, subtract that many tokens and return true. - * Otherwise, return false. - * @param {number} cost - the number of tokens to try to spend. - * @returns {boolean} true if we had enough tokens; false otherwise. - * @memberof TaskQueue - */ - _spend (cost) { - if (cost <= this._tokenCount) { - this._tokenCount -= cost; - return true; - } - return false; - } - - /** - * Loop until the task queue is empty, running each task and spending tokens to do so. - * Any time the bucket can't afford the next task, delay asynchronously until it can. - * @memberof TaskQueue - */ - _runTasks () { - if (this._timeout) { - this._timer.clearTimeout(this._timeout); - this._timeout = null; - } - for (;;) { - const nextRecord = this._pendingTaskRecords.shift(); - if (!nextRecord) { - // We ran out of work. Go idle until someone adds another task to the queue. - return; - } - if (nextRecord.cost > this._maxTokens) { - throw new Error(`Task cost ${nextRecord.cost} is greater than bucket limit ${this._maxTokens}`); - } - // Refill before each task in case the time it took for the last task to run was enough to afford the next. - if (this._refillAndSpend(nextRecord.cost)) { - nextRecord.wrappedTask(); - } else { - // We can't currently afford this task. Put it back and wait until we can and try again. - this._pendingTaskRecords.unshift(nextRecord); - const tokensNeeded = Math.max(nextRecord.cost - this._tokenCount, 0); - const estimatedWait = Math.ceil(1000 * tokensNeeded / this._refillRate); - this._timeout = this._timer.setTimeout(this._runTasks, estimatedWait); - return; - } - } - } -} - -module.exports = TaskQueue; diff --git a/packages/scratch-vm/test/unit/util_task-queue.js b/packages/scratch-vm/test/unit/util_task-queue.js deleted file mode 100644 index 4995bca025..0000000000 --- a/packages/scratch-vm/test/unit/util_task-queue.js +++ /dev/null @@ -1,192 +0,0 @@ -const test = require('tap').test; - -const TaskQueue = require('../../src/util/task-queue'); - -const MockTimer = require('../fixtures/mock-timer'); -const testCompare = require('../fixtures/test-compare'); - -// Max tokens = 1000 -// Refill 1000 tokens per second (1 per millisecond) -// Token bucket starts empty -// Max total cost of queued tasks = 10000 tokens = 10 seconds -const makeTestQueue = () => { - const bukkit = new TaskQueue(1000, 1000, { - startingTokens: 0, - maxTotalCost: 10000 - }); - - const mockTimer = new MockTimer(); - bukkit._timer = mockTimer; - mockTimer.start(); - - return bukkit; -}; - -test('spec', t => { - t.type(TaskQueue, 'function'); - const bukkit = makeTestQueue(); - - t.type(bukkit, 'object'); - - t.type(bukkit.length, 'number'); - t.type(bukkit.do, 'function'); - t.type(bukkit.cancel, 'function'); - t.type(bukkit.cancelAll, 'function'); - - t.end(); -}); - -test('constructor', t => { - t.ok(new TaskQueue(1, 1)); - t.ok(new TaskQueue(1, 1, {})); - t.ok(new TaskQueue(1, 1, {startingTokens: 0})); - t.ok(new TaskQueue(1, 1, {maxTotalCost: 999})); - t.ok(new TaskQueue(1, 1, {startingTokens: 0, maxTotalCost: 999})); - t.end(); -}); - -test('run tasks', async t => { - const bukkit = makeTestQueue(); - - const taskResults = []; - - const promises = [ - bukkit.do(() => { - taskResults.push('a'); - testCompare(t, bukkit._timer.timeElapsed(), '>=', 50, 'Costly task must wait'); - }, 50), - bukkit.do(() => { - taskResults.push('b'); - testCompare(t, bukkit._timer.timeElapsed(), '>=', 60, 'Tasks must run in serial'); - }, 10), - bukkit.do(() => { - taskResults.push('c'); - testCompare(t, bukkit._timer.timeElapsed(), '<=', 70, 'Cheap task should run soon'); - }, 1) - ]; - - // advance 10 simulated milliseconds per JS tick - while (bukkit.length > 0) { - await bukkit._timer.advanceMockTimeAsync(10); - } - - return Promise.all(promises).then(() => { - t.same(taskResults, ['a', 'b', 'c'], 'All tasks must run in correct order'); - t.end(); - }); -}); - -test('cancel', async t => { - const bukkit = makeTestQueue(); - - const taskResults = []; - const goodCancelMessage = 'Task was canceled correctly'; - const afterCancelMessage = 'Task was run correctly'; - const cancelTaskPromise = bukkit.do( - () => { - taskResults.push('nope'); - }, 999); - const cancelCheckPromise = cancelTaskPromise.then( - () => { - t.fail('Task should have been canceled'); - }, - () => { - taskResults.push(goodCancelMessage); - } - ); - const keepTaskPromise = bukkit.do( - () => { - taskResults.push(afterCancelMessage); - testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks'); - }, 5); - - // give the bucket a chance to make a mistake - await bukkit._timer.advanceMockTimeAsync(1); - - t.equal(bukkit.length, 2); - const taskWasCanceled = bukkit.cancel(cancelTaskPromise); - t.ok(taskWasCanceled); - t.equal(bukkit.length, 1); - - while (bukkit.length > 0) { - await bukkit._timer.advanceMockTimeAsync(1); - } - - return Promise.all([cancelCheckPromise, keepTaskPromise]).then(() => { - t.same(taskResults, [goodCancelMessage, afterCancelMessage]); - t.end(); - }); -}); - -test('cancelAll', async t => { - const bukkit = makeTestQueue(); - - const taskResults = []; - const goodCancelMessage1 = 'Task1 was canceled correctly'; - const goodCancelMessage2 = 'Task2 was canceled correctly'; - - const promises = [ - bukkit.do(() => taskResults.push('nope'), 999).then( - () => { - t.fail('Task1 should have been canceled'); - }, - () => { - taskResults.push(goodCancelMessage1); - } - ), - bukkit.do(() => taskResults.push('nah'), 999).then( - () => { - t.fail('Task2 should have been canceled'); - }, - () => { - taskResults.push(goodCancelMessage2); - } - ) - ]; - - // advance time, but not enough that any task should run - await bukkit._timer.advanceMockTimeAsync(100); - - bukkit.cancelAll(); - - // advance enough that both tasks would run if they hadn't been canceled - await bukkit._timer.advanceMockTimeAsync(10000); - - return Promise.all(promises).then(() => { - t.same(taskResults, [goodCancelMessage1, goodCancelMessage2], 'Tasks should cancel in order'); - t.end(); - }); -}); - -test('max total cost', async t => { - const bukkit = makeTestQueue(); - - let numTasks = 0; - - const task = () => ++numTasks; - - // Fill the queue - for (let i = 0; i < 10; ++i) { - bukkit.do(task, 1000); - } - - // This one should be rejected because the queue is full - bukkit - .do(task, 1000) - .then( - () => { - t.fail('Full queue did not reject task'); - }, - () => { - t.pass(); - } - ); - - while (bukkit.length > 0) { - await bukkit._timer.advanceMockTimeAsync(1000); - } - - // this should be 10 if the last task is rejected or 11 if it runs - t.equal(numTasks, 10); - t.end(); -}); diff --git a/packages/task-herder/.gitignore b/packages/task-herder/.gitignore new file mode 100644 index 0000000000..3ae196d246 --- /dev/null +++ b/packages/task-herder/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +/coverage +/node_modules +/dist +/dist-ssr +/test-results +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/task-herder/LICENSE b/packages/task-herder/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/task-herder/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/task-herder/README.md b/packages/task-herder/README.md new file mode 100644 index 0000000000..7be51fa885 --- /dev/null +++ b/packages/task-herder/README.md @@ -0,0 +1,120 @@ +# Task Herder + +Task Herder helps you herd your asynchronous ~~cats~~ ~~kats~~ tasks. Its main job is to provide a way to queue up +tasks and run them with configurable throttling and concurrency limits. + +## Concepts + +The queue operates according to the Token Bucket algorithm. The general idea is that you have a "bucket" that can hold +a certain number of "tokens". Each task "costs" a certain number of tokens (default: 1) to run. Tokens are added to +the bucket at a certain rate (the "refill rate") up to the maximum capacity of the bucket. + +When a task is added to the queue, if there are enough tokens in the bucket, the task will "spend" the required number +of tokens and be eligible to run. If there are not enough tokens, the task will wait in the queue until enough tokens +are available. Tasks are run in the order they were added to the queue (FIFO), so keep in mind that an expensive task +may delay cheaper tasks behind it until it can run. + +Eligible tasks are run immediately, up to the maximum concurrency limit. This is useful if you need to limit concurrent +resource usage with tasks that take an unpredictable amount of time. For example, you may want to make no more than 5 +concurrent connections to a particular service at a time. If the concurrency limit is reached, eligible tasks will wait +their turn. This stage of processing is also FIFO. + +Once a task makes it through both the token and concurrency checks, it is executed. The Promise generated by the Task +Queue will then resolve or reject based on the outcome of the task. + +## Usage + +```javascript +import TaskQueue from 'task-herder' + +const queue = new TaskQueue({ + // The maximum number of tokens in the bucket controls the burst limit + burstLimit: 10, + + // Rate at which tokens are added to the bucket (tokens per second) controls the sustained rate + sustainRate: 5, + + // Initial number of tokens in the bucket + // Default: burstLimit (i.e., start with a full bucket) + startingTokens: 5, + + // Reject a task if it would cause the total queue cost to exceed this limit + // Default: no limit + queueCostLimit: 50, + + // Number of tasks that can be processed concurrently + // Default: 1 (run tasks serially) + concurrency: 3, +}) + +// Using `await` syntax +try { + const response = await queue.do( + // Your async task here + () => fetch('https://example.com/data'), + { + // Optional cost of this task (default: 1) + cost: 2, + }, + ) + // Handle successful response +} catch (error) { + // Handle error +} + +// Using Promise syntax +queue + .do( + // Your async task here + () => fetch('https://example.com/data'), + { + // Optional cost of this task (default: 1) + cost: 2, + }, + ) + .then(response => { + // Handle successful response + }) + .catch(error => { + // Handle error + }) +``` + +You can also supply an `AbortSignal` to cancel a task before it starts: + +```javascript +const controller = new AbortController() +queue + .do( + () => + fetch('https://example.com/data', { + // Optionally, provide the same signal (or a different one) so it can interrupt the fetch request + signal: controller.signal, + }), + { + // Provide the signal to the task queue so it can cancel the task before it starts + signal: controller.signal, + }, + ) + .catch(error => { + if (error.name === 'AbortError') { + // Handle task cancellation + } else { + // Handle other errors + } + }) +``` + +Or, you can remove one task or all tasks from the queue before they start: + +```javascript +const taskPromise = queue.do(() => fetch('https://example.com/data')) +queue.cancel(taskPromise, optionalReason) // Remove a specific task from the queue +queue.cancelAll(optionalReason) // Remove all tasks from the queue +``` + +## Donate + +We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a +[donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, +and resource development efforts. Donations of any size are appreciated. Thank you! diff --git a/packages/task-herder/eslint.config.mjs b/packages/task-herder/eslint.config.mjs new file mode 100644 index 0000000000..9dd3a9a806 --- /dev/null +++ b/packages/task-herder/eslint.config.mjs @@ -0,0 +1,23 @@ +import { eslintConfigScratch } from 'eslint-config-scratch' +import { globalIgnores } from 'eslint/config' +import globals from 'globals' + +export default eslintConfigScratch.defineConfig( + eslintConfigScratch.recommended, + { + files: ['src/**'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['*'], // files in the root of the package (not subdirectories) + languageOptions: { + globals: globals.node, + }, + }, + globalIgnores(['coverage/**', 'dist/**', 'node_modules/**']), +) diff --git a/packages/task-herder/index.html b/packages/task-herder/index.html new file mode 100644 index 0000000000..060d38f954 --- /dev/null +++ b/packages/task-herder/index.html @@ -0,0 +1,12 @@ + + + + + + task-herder + + +
+ + + diff --git a/packages/task-herder/package.json b/packages/task-herder/package.json new file mode 100644 index 0000000000..e891390360 --- /dev/null +++ b/packages/task-herder/package.json @@ -0,0 +1,55 @@ +{ + "name": "@scratch/task-herder", + "version": "0.0.0", + "description": "An asynchronous task runner with configurable rate limiting / throttling and concurrency control.", + "keywords": [ + "rate limit", + "throttle", + "task", + "queue", + "concurrency", + "concurrent" + ], + "author": "Scratch Foundation", + "license": "AGPL-3.0-only", + "private": true, + "type": "module", + "files": [ + "LICENSE", + "README.md", + "dist" + ], + "main": "dist/task-herder.umd.cjs", + "module": "dist/task-herder.js", + "exports": { + ".": { + "import": "./dist/task-herder.js", + "require": "./dist/task-herder.umd.cjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc --project tsconfig.build.json && vite build", + "format": "prettier --write . && eslint --fix", + "lint": "eslint && prettier --check .", + "preview": "vite preview", + "test": "vitest run --coverage" + }, + "devDependencies": { + "@vitest/coverage-v8": "4.0.7", + "eslint": "9.39.1", + "eslint-config-scratch": "12.0.24", + "prettier": "3.6.2", + "typescript": "~5.9.3", + "unplugin-dts": "1.0.0-beta.6", + "vite": "npm:rolldown-vite@7.1.14", + "vitest": "4.0.7" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.1.14" + }, + "dependencies": { + "p-limit": "7.2.0" + } +} diff --git a/packages/task-herder/prettier.config.mjs b/packages/task-herder/prettier.config.mjs new file mode 100644 index 0000000000..1b1da18975 --- /dev/null +++ b/packages/task-herder/prettier.config.mjs @@ -0,0 +1,3 @@ +import { prettierConfigScratch } from 'eslint-config-scratch' + +export default prettierConfigScratch.recommended diff --git a/packages/task-herder/src/CancelReason.ts b/packages/task-herder/src/CancelReason.ts new file mode 100644 index 0000000000..26f34df07d --- /dev/null +++ b/packages/task-herder/src/CancelReason.ts @@ -0,0 +1,6 @@ +export const CancelReason = { + QueueCostLimitExceeded: 'Queue cost limit exceeded', + Aborted: 'Task aborted', + Cancel: 'Task cancelled', + TaskTooExpensive: 'Task cost exceeds maximum bucket size', +} as const diff --git a/packages/task-herder/src/PromiseWithResolvers.ts b/packages/task-herder/src/PromiseWithResolvers.ts new file mode 100644 index 0000000000..b3d9001381 --- /dev/null +++ b/packages/task-herder/src/PromiseWithResolvers.ts @@ -0,0 +1,25 @@ +/** + * Equivalent to `Promise.withResolvers`, which is not yet widely available. + * @todo Remove this function when `Promise.withResolvers` is widely available, likely around September 2026. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers} + * @returns An object containing the promise along with its resolve and reject functions. + */ +export function PromiseWithResolvers(): { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (reason?: unknown) => void +} { + let resolve + let reject + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + // The `as unknown as` casts are necessary because TypeScript doesn't + // know that the Promise executor is called synchronously. + return { + promise, + resolve: resolve as unknown as (value: T | PromiseLike) => void, + reject: reject as unknown as (reason?: unknown) => void, + } +} diff --git a/packages/task-herder/src/TaskQueue.ts b/packages/task-herder/src/TaskQueue.ts new file mode 100644 index 0000000000..8327161625 --- /dev/null +++ b/packages/task-herder/src/TaskQueue.ts @@ -0,0 +1,200 @@ +import pLimit, { type LimitFunction } from 'p-limit' +import { CancelReason } from './CancelReason' +import { TaskRecord, type TaskOptions } from './TaskRecord' + +export interface BucketOptions { + /** The maximum number of tokens in the bucket controls the burst limit */ + burstLimit: number + /** Rate at which tokens are added to the bucket (tokens per second) controls the sustained rate */ + sustainRate: number + /** Initial number of tokens in the bucket (default to a full bucket) */ + startingTokens?: number + /** Reject a task if it would cause the total queue cost to exceed this limit (default: no limit) */ + queueCostLimit?: number + /** Number of tasks that can be processed concurrently (default: 1) */ + concurrency?: number +} + +/** + * Run tasks with rate and concurrency limits. + * The rate limit is based on the token bucket algorithm. In this algorithm, a "bucket" holds a certain number of + * tokens, which represent the capacity to perform work. The bucket gradually refills with tokens at a fixed rate, up + * to a maximum capacity. Each task "costs" a certain number of tokens; if insufficient tokens are available, the task + * must wait until enough tokens have accumulated. + * In addition, a concurrency limit controls how many tasks can be run simultaneously. If the concurrency limit is + * reached, additional tasks must wait until a running task completes even if there are enough tokens available. + * @see {@link https://en.wikipedia.org/wiki/Token_bucket} for more information about the algorithm. + */ +export class TaskQueue { + private readonly burstLimit: number + private readonly sustainRate: number + private readonly queueCostLimit: number + private readonly concurrencyLimiter: LimitFunction + private readonly boundRunTasks = this.runTasks.bind(this) + private tokenCount: number + + private pendingTaskRecords: TaskRecord[] = [] + private timeout: number | null = null + private lastRefillTime: number = Date.now() + + constructor(options: BucketOptions) { + this.burstLimit = options.burstLimit + this.sustainRate = options.sustainRate + this.tokenCount = options.startingTokens ?? options.burstLimit + this.queueCostLimit = options.queueCostLimit ?? Infinity + this.concurrencyLimiter = pLimit(options.concurrency ?? 1) + } + + /** @returns The number of tasks currently in the queue */ + get length(): number { + return this.pendingTaskRecords.length + } + + /** + * Adds a task to the queue. The task will first wait until enough tokens are available, then will wait its turn in + * the concurrency queue. + * @param task The task to be added to the queue. + * @param taskOptions Options for queueing the task, such as the task cost. + * @returns A promise for the task's result. + */ + do(task: () => T | Promise, taskOptions: TaskOptions = {}): Promise { + const taskRecord = new TaskRecord(task, taskOptions) + + if (taskRecord.cost > this.burstLimit) { + return Promise.reject(new Error(CancelReason.TaskTooExpensive)) + } + + if (this.queueCostLimit < Infinity) { + const proposedQueueCost = this.pendingTaskRecords.reduce((sum, record) => sum + record.cost, taskRecord.cost) + if (proposedQueueCost > this.queueCostLimit) { + return Promise.reject(new Error(CancelReason.QueueCostLimitExceeded)) + } + } + + this.pendingTaskRecords.push(taskRecord) + + taskOptions.signal?.addEventListener('abort', () => { + this.cancel(taskRecord.promise, new Error(CancelReason.Aborted)) + }) + + // If the queue was empty, we need to prime the pump + if (this.pendingTaskRecords.length === 1) { + void this.runTasks() + } + + return taskRecord.promise + } + + /** + * Cancel a task and remove it from the queue. + * @param taskPromise - The promise of the task to cancel. + * @param [reason] - The reason for cancellation. + * @returns True if the task was found and cancelled, false otherwise. + */ + cancel(taskPromise: Promise, reason?: Error): boolean { + const taskIndex = this.pendingTaskRecords.findIndex(record => record.promise === taskPromise) + if (taskIndex !== -1) { + const [taskRecord] = this.pendingTaskRecords.splice(taskIndex, 1) + taskRecord.cancel(reason ?? new Error(CancelReason.Cancel)) + if (taskIndex === 0 && this.pendingTaskRecords.length > 0) { + void this.runTasks() + } + return true + } + return false + } + + /** + * Cancel all pending tasks and clear the queue. + * @param [reason] - The reason for cancellation. + * @returns The number of tasks that were cancelled. + */ + cancelAll(reason?: Error): number { + if (this.timeout !== null) { + clearTimeout(this.timeout) + this.timeout = null + } + const oldTasks = this.pendingTaskRecords + this.pendingTaskRecords = [] + reason = reason ?? new Error(CancelReason.Cancel) + oldTasks.forEach(taskRecord => { + taskRecord.cancel(reason) + }) + return oldTasks.length + } + + /** + * Short-hand for calling refill() followed by spend(). + * @param cost The number of tokens to spend. + * @returns True if the tokens were successfully spent, false otherwise. + */ + private refillAndSpend(cost: number): boolean { + this.refill() + return this.spend(cost) + } + + /** + * Refill the token bucket based on the time elapsed since the last refill. + */ + private refill(): void { + const now = Date.now() + const timeSinceRefill = now - this.lastRefillTime + if (timeSinceRefill <= 0) { + return + } + + this.lastRefillTime = now + const tokensToAdd = (timeSinceRefill / 1000) * this.sustainRate + this.tokenCount = Math.min(this.burstLimit, this.tokenCount + tokensToAdd) + } + + /** + * Attempt to spend tokens from the bucket. + * @param cost The number of tokens to spend. + * @returns True if the tokens were successfully spent, false otherwise. + */ + private spend(cost: number): boolean { + if (this.tokenCount >= cost) { + this.tokenCount -= cost + return true + } + return false + } + + /** + * Run tasks from the queue as tokens become available. + */ + private runTasks(): void { + if (this.timeout !== null) { + clearTimeout(this.timeout) + this.timeout = null + } + + for (;;) { + const nextRecord = this.pendingTaskRecords.shift() + if (!nextRecord) { + // No more tasks to run + return + } + + if (nextRecord.cost > this.burstLimit) { + // This should have been caught when the task was added + nextRecord.cancel(new Error(CancelReason.TaskTooExpensive)) + continue + } + + // Refill before each task in case the time it took for the last task to run was enough to afford the next. + if (this.refillAndSpend(nextRecord.cost)) { + // Run the task within the concurrency limiter + void this.concurrencyLimiter(nextRecord.run) + } else { + // We can't currently afford this task. Put it back and wait until we can, then try again. + this.pendingTaskRecords.unshift(nextRecord) + const tokensNeeded = Math.max(nextRecord.cost - this.tokenCount, 0) + const estimatedWait = Math.ceil((1000 * tokensNeeded) / this.sustainRate) + this.timeout = setTimeout(this.boundRunTasks, estimatedWait) + return + } + } + } +} diff --git a/packages/task-herder/src/TaskRecord.ts b/packages/task-herder/src/TaskRecord.ts new file mode 100644 index 0000000000..3fd30eb3fd --- /dev/null +++ b/packages/task-herder/src/TaskRecord.ts @@ -0,0 +1,41 @@ +import { PromiseWithResolvers } from './PromiseWithResolvers'; + +export interface TaskOptions { + /** Cost of the task in tokens (default: 1) */ + cost?: number; + /** If set, the task will be aborted if this signal is triggered */ + signal?: AbortSignal; +} +export class TaskRecord { + /** The cost of the task in tokens */ + public readonly cost: number; + /** The promise wrapping the task */ + public readonly promise: Promise; + /** Run the task and settle the promise */ + public readonly run: () => Promise; + /** Cancel the task and reject the promise */ + public readonly cancel: (e: Error) => void; + + /** + * @param task The task to wrap. + * @param options The options for the task. + */ + constructor(task: () => T | Promise, options: TaskOptions = {}) { + this.cost = options.cost ?? 1; + + const { promise, resolve, reject } = PromiseWithResolvers(); + + this.promise = promise; + this.cancel = e => { + reject(e); + }; + this.run = async () => { + try { + const result = await task(); + resolve(result); + } catch (error) { + reject(error); + } + }; + } +} diff --git a/packages/task-herder/src/demo.ts b/packages/task-herder/src/demo.ts new file mode 100644 index 0000000000..04bb038516 --- /dev/null +++ b/packages/task-herder/src/demo.ts @@ -0,0 +1,10 @@ +const app = document.querySelector('#app') +if (!app) throw new Error('Could not find app div!') +app.innerHTML = ` +
+

Task Herder

+

+ This will be a demo of the task-herder package. +

+
+` diff --git a/packages/task-herder/src/index.ts b/packages/task-herder/src/index.ts new file mode 100644 index 0000000000..e627aada72 --- /dev/null +++ b/packages/task-herder/src/index.ts @@ -0,0 +1,2 @@ +export { CancelReason } from './CancelReason' +export { type BucketOptions, TaskQueue } from './TaskQueue' diff --git a/packages/task-herder/test/basics.test.ts b/packages/task-herder/test/basics.test.ts new file mode 100644 index 0000000000..0697ec88f1 --- /dev/null +++ b/packages/task-herder/test/basics.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue, CancelReason } from '../src' +import { waitTicks } from './test-utilities' + +describe('basics', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should create an instance', () => { + const bucket = new TaskQueue({ + startingTokens: 3, + burstLimit: 10, + sustainRate: 1, + queueCostLimit: 20, + }) + expect(bucket).toBeInstanceOf(TaskQueue) + }) + it('should pass through task results', async () => { + const bucket = new TaskQueue({ + burstLimit: 10, + sustainRate: 1, + }) + const task = () => 'sync done' + const task2 = () => Promise.resolve('async done') + const task3 = () => { + throw new Error('task error') + } + const task4 = () => Promise.reject(new Error('async task error')) + + await waitTicks(1000) + + await expect(bucket.do(task)).resolves.toBe('sync done') + await expect(bucket.do(task2)).resolves.toBe('async done') + await expect(bucket.do(task3)).rejects.toThrow('task error') + await expect(bucket.do(task4)).rejects.toThrow('async task error') + }) + it('should reject a task that exceeds the burst limit', async () => { + const bucket = new TaskQueue({ + startingTokens: 3, + burstLimit: 2, + sustainRate: 1, + queueCostLimit: 20, + }) + const task = () => Promise.resolve('done') + + // This task is too big to ever fit in this tiny bucket + await expect(bucket.do(task, { cost: 3 })).rejects.toThrow(CancelReason.TaskTooExpensive) + + // This task fits + await expect(bucket.do(task, { cost: 2 })).resolves.toBe('done') + }) + it('should reject a task that pushes the queue past its cost limit', async () => { + const bucket = new TaskQueue({ + startingTokens: 3, + burstLimit: 10, + sustainRate: 1, + queueCostLimit: 10, + }) + const task = () => Promise.resolve('done') + + void bucket.do(task, { cost: 5 }) + void bucket.do(task, { cost: 4 }) + expect(bucket.length).toBe(2) + + // This task is small enough to fit in the burst limit but too big to fit in the queue with the other tasks + await expect(bucket.do(task, { cost: 2 })).rejects.toThrow(CancelReason.QueueCostLimitExceeded) + expect(bucket.length).toBe(2) + + // This task fits + void bucket.do(task, { cost: 1 }) + expect(bucket.length).toBe(3) + }) + it('should reject a task even if there are cost limit shenanigans', async () => { + const bucket = new TaskQueue({ + startingTokens: 0, + burstLimit: 100, + sustainRate: 1000, + queueCostLimit: 1000, + }) + const task = () => Promise.resolve('done') + + // The task is below the burst limit right now + const taskPromise = bucket.do(task, { cost: 10 }) + + // Don't try this at home + ;(bucket as unknown as Record).burstLimit = 1 + + // Now let things settle + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // The task became too expensive due to the burst limit change after being queued but before running + // Right now this is "impossible" but could be relevant if we ever allow changes to the burst limit or task cost + await expect(taskPromise).rejects.toThrow(CancelReason.TaskTooExpensive) + }) +}) diff --git a/packages/task-herder/test/cancel.test.ts b/packages/task-herder/test/cancel.test.ts new file mode 100644 index 0000000000..cf97ecafed --- /dev/null +++ b/packages/task-herder/test/cancel.test.ts @@ -0,0 +1,182 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue, CancelReason } from '../src' +import { waitTicks, makeTask } from './test-utilities' + +describe('cancel()', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should cancel a pending task and return true', async () => { + const bucket = new TaskQueue({ + startingTokens: 1, // allow first task to start immediately + burstLimit: 5, + sustainRate: 1, // slow refill so second stays pending initially + concurrency: 1, + }) + + const states: Record = {} + const p1 = bucket.do(makeTask(states, 'task1', 5), { cost: 1 }) + const p2 = bucket.do(makeTask(states, 'task2', 5), { cost: 2 }) + + // Let queue settle; first task should be started, second pending + await waitTicks(1000) + expect(states.task1).toEqual('started') + expect(states.task2).toBeUndefined() + + const didCancelTask = bucket.cancel(p2) + expect(didCancelTask).toBe(true) + + // Advance enough time that the second task would have started and finished if not cancelled + vi.advanceTimersByTime(100) + await waitTicks(1000) + + expect(states.task1).toEqual('finished') + expect(states.task2).toBeUndefined() + await expect(p1).resolves.toBeUndefined() + await expect(p2).rejects.toThrow(CancelReason.Cancel) + }) + + it('should return false when cancelling unknown promise', async () => { + const bucket = new TaskQueue({ + startingTokens: 0, + burstLimit: 5, + sustainRate: 1000, + concurrency: 1, + }) + const fakePromise = Promise.resolve() + await waitTicks(1000) + const didCancelTask = bucket.cancel(fakePromise) + expect(didCancelTask).toBe(false) + }) + + it('should cancel a pending task without disrupting the queue', async () => { + const bucket = new TaskQueue({ + startingTokens: 1, + burstLimit: 5, + sustainRate: 1000, + concurrency: 1, + }) + const states: Record = {} + const p1 = bucket.do(makeTask(states, 'task1', 5), { cost: 1 }) + const p2 = bucket.do(makeTask(states, 'task2', 5), { cost: 1 }) + const p3 = bucket.do(makeTask(states, 'task3', 5), { cost: 1 }) + + await waitTicks(1000) + expect(states.task1).toEqual('started') + expect(states.task2).toBeUndefined() + expect(states.task3).toBeUndefined() + + // Cancel second task (pending) + bucket.cancel(p2) + + // Advance timers so first finishes and third can start + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // Third should have started after first finished because second was cancelled + expect(states.task1).toEqual('finished') + await expect(p2).rejects.toThrow(CancelReason.Cancel) + expect(states.task3).toEqual('started') + + // Finish third + vi.advanceTimersByTime(10) + await waitTicks(1000) + expect(states.task1).toEqual('finished') + expect(states.task2).toBeUndefined() + expect(states.task3).toEqual('finished') + + await expect(p1).resolves.toBeUndefined() + await expect(p3).resolves.toBeUndefined() + }) +}) + +describe('cancelAll()', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should cancel all pending tasks and return their count', async () => { + const bucket = new TaskQueue({ + startingTokens: 0, + burstLimit: 5, + sustainRate: 1, + concurrency: 2, + }) + const states: Record = {} + const p1 = bucket.do(makeTask(states, 'task1', 5), { cost: 1 }) + const p2 = bucket.do(makeTask(states, 'task2', 5), { cost: 1 }) + const p3 = bucket.do(makeTask(states, 'task3', 5), { cost: 1 }) + + // Let queue settle and time advance a bit + await waitTicks(1000) + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // None should start because startingTokens=0 and no time advanced to refill + expect(states.task1).toBeUndefined() + expect(states.task2).toBeUndefined() + expect(states.task3).toBeUndefined() + + // Cancel all pending tasks + const cancelledCount = bucket.cancelAll() + expect(cancelledCount).toBe(3) + + // Advance timers to allow any tasks to run if they weren't properly cancelled + vi.advanceTimersByTime(100) + await waitTicks(1000) + + // Verify none started and all were cancelled + expect(states.task1).toBeUndefined() + expect(states.task2).toBeUndefined() + expect(states.task3).toBeUndefined() + + await expect(p1).rejects.toThrow(CancelReason.Cancel) + await expect(p2).rejects.toThrow(CancelReason.Cancel) + await expect(p3).rejects.toThrow(CancelReason.Cancel) + }) +}) + +describe('abortSignal option', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should abort a pending task using abortSignal', async () => { + const bucket = new TaskQueue({ + startingTokens: 1, + burstLimit: 5, + sustainRate: 1, + concurrency: 1, + }) + const states: Record = {} + const controller = new AbortController() + const p1 = bucket.do(makeTask(states, 'task1', 5), { cost: 1 }) + const p2 = bucket.do(makeTask(states, 'task2', 5), { cost: 2, signal: controller.signal }) + + await waitTicks(1000) + expect(states.task1).toEqual('started') + expect(states.task2).toBeUndefined() + + // Abort second before it runs + controller.abort() + + // Let first finish + vi.advanceTimersByTime(10) + await waitTicks(1000) + + expect(states.task1).toEqual('finished') + expect(states.task2).toBeUndefined() // never started + await expect(p1).resolves.toBeUndefined() + await expect(p2).rejects.toThrow(CancelReason.Aborted) + }) +}) diff --git a/packages/task-herder/test/concurrency-1.test.ts b/packages/task-herder/test/concurrency-1.test.ts new file mode 100644 index 0000000000..2e53375478 --- /dev/null +++ b/packages/task-herder/test/concurrency-1.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue } from '../src' +import { waitTicks, makeTask } from './test-utilities' + +describe('concurrency limit 1', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should obey the concurrency limit', async () => { + const bucket = new TaskQueue({ + startingTokens: 10, + burstLimit: 10, + sustainRate: 1, // refill isn't relevant for this test + queueCostLimit: 20, + concurrency: 1, + }) + + const taskStates: Record = {} + + void bucket.do(makeTask(taskStates, 'task1', 10)) + void bucket.do(makeTask(taskStates, 'task2', 10)) + void bucket.do(makeTask(taskStates, 'task3', 10)) + + // Wait many ticks, but zero time, to allow the queueing machinery to settle + await waitTicks(1000) + + // Since no time has passed, nothing should have finished yet + expect(taskStates.task1).toEqual('started') + expect(taskStates.task2).toBeUndefined() + expect(taskStates.task3).toBeUndefined() + + // Advance time by 10ms to allow the first task to complete + // Since concurrency=1, only the first task should complete, even though the bucket has tokens for more + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // After 10ms, the first task should be done and the second should start. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('started') + expect(taskStates.task3).toBeUndefined() + + // Advance time by another 10ms to allow the second task to complete + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // After 20ms, the second task should be done. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('started') + + // Advance time by another 10ms to allow the third task to complete + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // After 30ms, all tasks should be done. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('finished') + }) +}) diff --git a/packages/task-herder/test/concurrency-2.test.ts b/packages/task-herder/test/concurrency-2.test.ts new file mode 100644 index 0000000000..897524b26c --- /dev/null +++ b/packages/task-herder/test/concurrency-2.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue } from '../src' +import { waitTicks, makeTask } from './test-utilities' + +describe('concurrency limit 2', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should obey the concurrency limit', async () => { + const bucket = new TaskQueue({ + startingTokens: 10, + burstLimit: 10, + sustainRate: 1, // refill isn't relevant for this test + queueCostLimit: 20, + concurrency: 2, + }) + + const taskStates: Record = {} + + void bucket.do(makeTask(taskStates, 'task1', 10)) + void bucket.do(makeTask(taskStates, 'task2', 10)) + void bucket.do(makeTask(taskStates, 'task3', 10)) + + // Wait many ticks, but zero time, to allow the queueing machinery to settle + await waitTicks(1000) + + // Since no time has passed, we shouldn't have any results yet + expect(taskStates.task1).toEqual('started') + expect(taskStates.task2).toEqual('started') + expect(taskStates.task3).toBeUndefined() + + // Advance time by 10ms to allow the first task to complete + // Since concurrency=2, both the first and second tasks should complete + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // After 10ms, the first task should be done. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('started') + + // Advance time by another 10ms to allow the third task to complete + vi.advanceTimersByTime(10) + await waitTicks(1000) + + // After 30ms, all tasks should be done. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('finished') + }) +}) diff --git a/packages/task-herder/test/rate-and-concurrency.test.ts b/packages/task-herder/test/rate-and-concurrency.test.ts new file mode 100644 index 0000000000..409514275c --- /dev/null +++ b/packages/task-herder/test/rate-and-concurrency.test.ts @@ -0,0 +1,64 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue } from '../src' +import { waitTicks, makeTask } from './test-utilities' + +describe('rate and concurrency limits', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should obey the limits', async () => { + const bucket = new TaskQueue({ + startingTokens: 30, + burstLimit: 20, + sustainRate: 1000, // 1 token per ms + queueCostLimit: 60, + concurrency: 2, + }) + + const taskStates: Record = {} + + void bucket.do(makeTask(taskStates, 'task1', 10), { cost: 10 }) + void bucket.do(makeTask(taskStates, 'task2', 10), { cost: 10 }) + void bucket.do(makeTask(taskStates, 'task3', 10), { cost: 10 }) + void bucket.do(makeTask(taskStates, 'task4', 10), { cost: 20 }) + + // Wait many ticks, but zero time, to allow the queueing machinery to settle + // We can afford 3 tasks immediately, but concurrency=2 means only 2 can start + // Remaining tokens = 30 to start - 10 for task1 - 10 for task2 = 10 + await waitTicks(1000) + expect(taskStates.task1).toEqual('started') + expect(taskStates.task2).toEqual('started') + expect(taskStates.task3).toBeUndefined() + expect(taskStates.task4).toBeUndefined() + + // After 10ms, the first burst of tasks should be done. + // We can now afford to start task3 (cost 10), but not task4 (cost 20) yet. + // Remaining tokens = 10 + 10 replenished - 10 for task3 = 10 + await vi.advanceTimersByTimeAsync(10) + await waitTicks(1000) + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('started') + expect(taskStates.task4).toBeUndefined() + + // After another 10ms, the third task should be done and we can start task4 (cost 20) + // Remaining tokens = 10 + 10 replenished - 20 for task4 = 0 + await vi.advanceTimersByTimeAsync(10) + await waitTicks(1000) + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('finished') + expect(taskStates.task4).toEqual('started') + + // Advance time by another 10ms to allow task4 to complete + await vi.advanceTimersByTimeAsync(10) + await waitTicks(1000) + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('finished') + expect(taskStates.task4).toEqual('finished') + }) +}) diff --git a/packages/task-herder/test/rate.test.ts b/packages/task-herder/test/rate.test.ts new file mode 100644 index 0000000000..f1246fb0ff --- /dev/null +++ b/packages/task-herder/test/rate.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TaskQueue } from '../src' +import { waitTicks, makeTask } from './test-utilities' + +describe('rate limit', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should obey the rate limit', async () => { + const bucket = new TaskQueue({ + startingTokens: 1, + burstLimit: 2, + sustainRate: 1000, // 1 token per ms + concurrency: 99999, + }) + + const taskStates: Record = {} + + void bucket.do(makeTask(taskStates, 'task1'), { cost: 1 }) + void bucket.do(makeTask(taskStates, 'task2'), { cost: 2 }) + void bucket.do(makeTask(taskStates, 'task3'), { cost: 1 }) + + // Wait many ticks, but zero time, to allow the queueing machinery to settle + // Since `startingTokens` is 1, only the first task should run immediately + await waitTicks(1000) + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toBeUndefined() + expect(taskStates.task3).toBeUndefined() + + // Advance time by 1ms, which should refill 1 token + vi.advanceTimersByTime(1) + await waitTicks(1000) + + // That 1 token isn't enough for the second task to run yet + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toBeUndefined() + expect(taskStates.task3).toBeUndefined() + + // Advance time by another 1ms to refill another token + vi.advanceTimersByTime(1) + await waitTicks(1000) + + // The second task should have run by now. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toBeUndefined() + + // Advance time by another 1ms to allow the third task to complete + vi.advanceTimersByTime(1) + await waitTicks(1000) + + // After 3ms, all tasks should be done. + expect(taskStates.task1).toEqual('finished') + expect(taskStates.task2).toEqual('finished') + expect(taskStates.task3).toEqual('finished') + }) +}) diff --git a/packages/task-herder/test/test-utilities.ts b/packages/task-herder/test/test-utilities.ts new file mode 100644 index 0000000000..4a8285117b --- /dev/null +++ b/packages/task-herder/test/test-utilities.ts @@ -0,0 +1,25 @@ +export async function waitTicks(count: number): Promise { + for (let i = 0; i < count; i++) { + // `await ` always yields to the event loop, even if `` is not a Promise. + // `await Promise.resolve()` may yield more than once. + await void 0 // eslint-disable-line @typescript-eslint/await-thenable -- See detailed comment above. + } +} + +export async function waitTime(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} + +export function makeTask(taskStates: Record, name: string, delay = 0): () => Promise { + return async () => { + // console.log(`Starting task ${name} at ${Date.now()}`); + taskStates[name] = 'started' + if (delay > 0) { + await waitTime(delay) + } + taskStates[name] = 'finished' + // console.log(`Finished task ${name} at ${Date.now()}`); + } +} diff --git a/packages/task-herder/tsconfig.build.json b/packages/task-herder/tsconfig.build.json new file mode 100644 index 0000000000..ce485f2d59 --- /dev/null +++ b/packages/task-herder/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["test/**"] +} diff --git a/packages/task-herder/tsconfig.json b/packages/task-herder/tsconfig.json new file mode 100644 index 0000000000..d7cd2530d5 --- /dev/null +++ b/packages/task-herder/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "test"] +} diff --git a/packages/task-herder/vite.config.js b/packages/task-herder/vite.config.js new file mode 100644 index 0000000000..f52c6d5b8b --- /dev/null +++ b/packages/task-herder/vite.config.js @@ -0,0 +1,69 @@ +/// +import dts from 'unplugin-dts/vite' +import { defineConfig } from 'vite' +import packageJson from './package.json' + +// Externalize all dependencies, peerDependencies, and optionalDependencies, but not devDependencies. +// Inspired in part by `davidmyersdev/vite-plugin-externalize-deps`. +// Note that recent versions of Vite automatically externalize Node built-in modules. +// Adding Node built-ins here suppresses warnings, but I say we want those warnings... +const externalDeps = [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.peerDependencies || {}), + ...Object.keys(packageJson.optionalDependencies || {}), +].map(name => new RegExp(`^${name}(?:/.*)?$`)) + +export default defineConfig({ + build: { + lib: { + entry: 'src/index.ts', + name: 'TaskHerder', + fileName: 'task-herder', + }, + rolldownOptions: { + external: externalDeps, + output: { + /** + * Customize the global variable names for externalized dependencies in UMD builds. + * @param {string} depName - The name of a dependency + * @returns {string} - The global variable name to use for the dependency + */ + globals: depName => { + switch (depName) { + case 'p-limit': + return 'pLimit' + default: + return depName + } + }, + }, + }, + }, + plugins: [ + // Generate TypeScript declaration files + dts({ + insertTypesEntry: true, + tsconfigPath: 'tsconfig.build.json', + }), + ], + test: { + coverage: { + exclude: ['dist/**', 'node_modules/**', 'test/**', 'vite.config.js'], + }, + reporters: [ + 'default', + + // This is mainly interesting for reporting GHA test results on PRs through `publish-unit-test-result-action`, + // but including it even outside of CI isn't expensive and might help catch configuration issues that would + // otherwise lead to "CI only" problems. + 'junit', + + // The `github-actions` reporter is added by default if running in GHA, but not if reporters are customized. + // For our purposes, it's somewhat redundant with `junit`, but it's kinda nice when looking at GHA output. + ...(process.env.GITHUB_ACTIONS === 'true' ? ['github-actions'] : []), + ], + outputFile: { + junit: 'test-results/junit.xml', + }, + }, +})