diff --git a/.changeset/seven-taxis-sip.md b/.changeset/seven-taxis-sip.md new file mode 100644 index 0000000..195ddfc --- /dev/null +++ b/.changeset/seven-taxis-sip.md @@ -0,0 +1,21 @@ +--- +'earwurm': minor +--- + +Fix issue "stopping" a `Sound` that was never "started". +Include a `neverStarted: boolean;` property in the `SoundEndedEvent`. +New `volume` change event for `Earwurm`, `Stack`, and `Sound`. +New `mute` change event for `Earwurm`, `Stack`, and `Sound`. +New `library` change event for `Earwurm`. +New `queue` change event for `Stack`. +New `speed` change event for `Sound`. +`speed` Setter now clamps the value between `0.25` and `4`. +New `progress` change event. +New `progress` Getter. +New `state > ending` value. +Renamed all `statechange` events to `state`. +No longer setting `mute = false` when "pausing". +Avoid re-initializing an existing `Stack` when `.add()` is passed an identical `id + path`. +Removed `LibraryKeys` type, instead using `StackIds[]` directly. +Now exporting `tokens` object with some usual values. +Updated `docs/api.md` to include details on all the newly added / changed code. diff --git a/.vscode/settings.json b/.vscode/settings.json index f6e8462..110faf4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2a95c..aef6f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# earwurm +# Earwurm Changelog ## 0.5.2 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..3779422 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,9 @@ +# Earwurm migration guide + +## 0.6.0 + +For more details on the released changes, please see [🐛 Various bug fixes](https://github.com/beefchimi/earwurm/pull/50). + +- Rename all instances of `statechange` to `state`. + - Example: `manager.on('statechange', () => {})` +- Replace any instances of `LibraryKeys` with the equivalent `StackIds[]`. diff --git a/docs/api.md b/docs/api.md index 51b0e76..face521 100644 --- a/docs/api.md +++ b/docs/api.md @@ -106,11 +106,21 @@ manager.activeEvents; // Returns the current `AudioContext > state`. This may trigger // immediately upon `new Earwurm()` if the `AudioContext` is // “unlocked” right away. -(event: 'statechange', listener: (state: ManagerState) => void) +(event: 'state', listener: (current: ManagerState) => void) + +// Event called whenever the `keys` property changes. This is useful +// to subscribe to changes in the internal “stack library”. +(event: 'library', listener: (newKeys: StackId[], oldKeys: StackId[]) => void) + +// Event called whenever the `volume` property changes. +(event: 'volume', listener: (level: number) => void) + +// Event called whenever the `mute` property changes. +(event: 'mute', listener: (muted: boolean) => void) // Event called whenever an error is occured on the `AudioContext`. // This could be a result of: failed to resume, failed to close. -(event: 'error', listener: (error: CombinedErrorMessage) => void) +(event: 'error', listener: (messages: CombinedErrorMessage) => void) ``` **Static members:** @@ -228,7 +238,17 @@ soundStack.activeEvents; // the `Stack`. As sounds cycle through their various states, the // `Stack` will determine if any `Sound` is currently `playing`. // Possible `StackState` values are: `idle`, `loading`, `playing`. -(event: 'statechange', listener: (state: StackState) => void) +(event: 'state', listener: (current: StackState) => void) + +// Event called whenever the `keys` property changes. This is useful +// to subscribe to changes in the internal “sound queue”. +(event: 'queue', listener: (newKeys: SoundId[], oldKeys: SoundId[]) => void) + +// Event called whenever the `volume` property changes. +(event: 'volume', listener: (level: number) => void) + +// Event called whenever the `mute` property changes. +(event: 'mute', listener: (muted: boolean) => void) // Event called whenever an error is occured on the `Stack`. // This could be a result of: failed to load the `path`. @@ -288,6 +308,11 @@ sound.volume = 1; // Mute / unmute this `Sound`. sound.mute = true || false; +// Set the `playbackRate` for this `Sound. +// This value is a number used to multiply the speed of playback. +// Default is `1`. Min is `0.25`. Max is `4`. +sound.speed; + // Toggle the “repetition” of the `Sound`. Will // repeat indefinitely if `true`, preventing the // `ended` event from firing. @@ -303,6 +328,9 @@ sound.volume; // Get a `boolean` for whether or not this `Sound` is “mute”. sound.mute; +// Get the current `playbackRate` for this `Sound. +sound.speed; + // Get a `boolean` for whether or not this `Sound` is // to repeat indefinitely. sound.loop; @@ -310,9 +338,16 @@ sound.loop; // Get the “total play time” for this `Sound`. sound.duration; +// Get the current `SoundProgressEvent` for this `Sound. +sound.progress; + // Get the current `state` for this `Sound`. Can be: -// `created`, `playing`, `paused`, or `stopping`. -sound.duration; +// `created`, `playing`, `paused`, `stopping`, or `ending`. +sound.state; + +// Get an array of all the events for this instance +// that currently have listeners attached. +sound.activeEvents; ``` **Events:** @@ -320,14 +355,28 @@ sound.duration; ```ts // Event called whenever `Sound > state` is changed. // Possible `SoundState` values are: -// `created`, `playing`, `paused`, and `stopping`. -(event: 'statechange', listener: (state: SoundState) => void) +// `created`, `playing`, `paused`, `stopping`, and `ending`. +(event: 'state', listener: (current: SoundState) => void) // Event called on the audio `source` node whenenver // a `Sound` reaches either it’s “end duration”, // or has been stopped / removed from the `Stack`. // This will NOT get called each time a “loop” repeats. (event: 'ended', listener: ({id, source}: SoundEndedEvent) => void) + +// Event called whenever the `volume` property changes. +(event: 'volume', listener: (level: number) => void) + +// Event called whenever the `mute` property changes. +(event: 'mute', listener: (muted: boolean) => void) + +// Event called for every animation frame while `playing`. +// Returns data representing: +// elapsed: “seconds” into the current iteration of this `Sound`. +// remaining: “seconds” until the end of the current iteration of this `Sound`. +// percentage: “percentage progressed” into the current iteration of this `Sound`. +// iterations: number of times this `Sound` has looped. +(event: 'progress', listener: ({elapsed, remaining, percentage, iterations}: SoundProgressEvent) => void) ``` ## Events API diff --git a/docs/examples.md b/docs/examples.md index 9b6e64c..21c08d8 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -119,14 +119,14 @@ setTimeout(() => appleSound?.play(), appleSoundDuration + durationBuffer); **Determining state values:** -While there is a dedicated `playing` property, you can obtain a more granular `state` by listening for the `statechange` event and checking the `state` property directly. +While there is a dedicated `playing` property, you can obtain a more granular `state` by listening for the `state` event and checking the `state` property directly. ```ts let capturedState = sound.state; let isPaused = capturedState === 'paused'; -sound.on('statechange', (state) => { - capturedState = sound.state; +sound.on('state', (state) => { + capturedState = state; isPaused = capturedState === 'paused'; }); ``` diff --git a/package-lock.json b/package-lock.json index 95bc82d..28e8679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,40 +1,40 @@ { "name": "earwurm", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "earwurm", - "version": "0.5.1", + "version": "0.5.2", "license": "ISC", "dependencies": { "emitten": "^0.6.1" }, "devDependencies": { - "@changesets/changelog-github": "^0.4.8", - "@changesets/cli": "^2.26.2", - "@types/node": "^20.10.0", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@vitest/coverage-v8": "^0.34.6", - "@vitest/ui": "^0.34.6", - "eslint": "^8.54.0", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-n": "^16.3.1", - "eslint-plugin-prettier": "^5.0.1", + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.1", + "@types/node": "^20.10.5", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@vitest/coverage-v8": "^1.1.0", + "@vitest/ui": "^1.1.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "^43.0.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.5.0", + "eslint-plugin-prettier": "^5.1.0", "eslint-plugin-promise": "^6.1.1", "happy-dom": "^12.10.3", - "prettier": "^3.1.0", - "typescript": "^5.3.2", - "vite": "^5.0.2", - "vite-plugin-dts": "^3.6.3", - "vitest": "^0.34.6" + "prettier": "^3.1.1", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vite-plugin-dts": "^3.6.4", + "vitest": "^1.1.0" }, "engines": { "node": ">=20.10.0", - "npm": ">=10.2.3" + "npm": ">=10.2.0" }, "peerDependencies": { "emitten": "^0.6.1" @@ -63,9 +63,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", @@ -75,6 +75,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", @@ -99,9 +108,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -111,9 +120,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -122,6 +131,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -129,16 +152,16 @@ "dev": true }, "node_modules/@changesets/apply-release-plan": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.4.tgz", - "integrity": "sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.0.tgz", + "integrity": "sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/config": "^2.3.1", - "@changesets/get-version-range-type": "^0.3.2", - "@changesets/git": "^2.0.0", - "@changesets/types": "^5.2.1", + "@changesets/config": "^3.0.0", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", @@ -165,69 +188,68 @@ } }, "node_modules/@changesets/assemble-release-plan": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.2.4.tgz", - "integrity": "sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.0.tgz", + "integrity": "sha512-4QG7NuisAjisbW4hkLCmGW2lRYdPrKzro+fCtZaILX+3zdUELSvYjpL4GTv0E4aM9Mef3PuIQp89VmHJ4y2bfw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "node_modules/@changesets/changelog-git": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.1.14.tgz", - "integrity": "sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.0.tgz", + "integrity": "sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1" + "@changesets/types": "^6.0.0" } }, "node_modules/@changesets/changelog-github": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz", - "integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.0.tgz", + "integrity": "sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==", "dev": true, "dependencies": { - "@changesets/get-github-info": "^0.5.2", - "@changesets/types": "^5.2.1", + "@changesets/get-github-info": "^0.6.0", + "@changesets/types": "^6.0.0", "dotenv": "^8.1.0" } }, "node_modules/@changesets/cli": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.2.tgz", - "integrity": "sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.27.1.tgz", + "integrity": "sha512-iJ91xlvRnnrJnELTp4eJJEOPjgpF3NOh4qeQehM6Ugiz9gJPRZ2t+TsXun6E3AMN4hScZKjqVXl0TX+C7AB3ZQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/apply-release-plan": "^6.1.4", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/changelog-git": "^0.1.14", - "@changesets/config": "^2.3.1", - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/get-release-plan": "^3.0.17", - "@changesets/git": "^2.0.0", - "@changesets/logger": "^0.0.5", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", - "@changesets/write": "^0.2.3", + "@changesets/apply-release-plan": "^7.0.0", + "@changesets/assemble-release-plan": "^6.0.0", + "@changesets/changelog-git": "^0.2.0", + "@changesets/config": "^3.0.0", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/get-release-plan": "^4.0.0", + "@changesets/git": "^3.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/pre": "^2.0.0", + "@changesets/read": "^0.6.0", + "@changesets/types": "^6.0.0", + "@changesets/write": "^0.3.0", "@manypkg/get-packages": "^1.1.3", - "@types/is-ci": "^3.0.0", "@types/semver": "^7.5.0", "ansi-colors": "^4.1.3", "chalk": "^2.1.0", + "ci-info": "^3.7.0", "enquirer": "^2.3.0", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", - "is-ci": "^3.0.1", "meow": "^6.0.0", "outdent": "^0.5.0", "p-limit": "^2.2.0", @@ -243,36 +265,36 @@ } }, "node_modules/@changesets/config": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.3.1.tgz", - "integrity": "sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.0.0.tgz", + "integrity": "sha512-o/rwLNnAo/+j9Yvw9mkBQOZySDYyOr/q+wptRLcAVGlU6djOeP9v1nlalbL9MFsobuBVQbZCTp+dIzdq+CLQUA==", "dev": true, "dependencies": { - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/logger": "^0.0.5", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.2" } }, "node_modules/@changesets/errors": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.1.4.tgz", - "integrity": "sha512-HAcqPF7snsUJ/QzkWoKfRfXushHTu+K5KZLJWPb34s4eCZShIf8BFO3fwq6KU8+G7L5KdtN2BzQAXOSXEyiY9Q==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", "dev": true, "dependencies": { "extendable-error": "^0.1.5" } }, "node_modules/@changesets/get-dependents-graph": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.3.6.tgz", - "integrity": "sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.0.0.tgz", + "integrity": "sha512-cafUXponivK4vBgZ3yLu944mTvam06XEn2IZGjjKc0antpenkYANXiiE6GExV/yKdsCnE8dXVZ25yGqLYZmScA==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "chalk": "^2.1.0", "fs-extra": "^7.0.1", @@ -280,9 +302,9 @@ } }, "node_modules/@changesets/get-github-info": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", - "integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", "dev": true, "dependencies": { "dataloader": "^1.4.0", @@ -290,35 +312,35 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.17.tgz", - "integrity": "sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.0.tgz", + "integrity": "sha512-9L9xCUeD/Tb6L/oKmpm8nyzsOzhdNBBbt/ZNcjynbHC07WW4E1eX8NMGC5g5SbM5z/V+MOrYsJ4lRW41GCbg3w==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/config": "^2.3.1", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", + "@changesets/assemble-release-plan": "^6.0.0", + "@changesets/config": "^3.0.0", + "@changesets/pre": "^2.0.0", + "@changesets/read": "^0.6.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } }, "node_modules/@changesets/get-version-range-type": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz", - "integrity": "sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", "dev": true }, "node_modules/@changesets/git": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@changesets/git/-/git-2.0.0.tgz", - "integrity": "sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.0.tgz", + "integrity": "sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.2", @@ -326,67 +348,67 @@ } }, "node_modules/@changesets/logger": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.0.5.tgz", - "integrity": "sha512-gJyZHomu8nASHpaANzc6bkQMO9gU/ib20lqew1rVx753FOxffnCrJlGIeQVxNWCqM+o6OOleCo/ivL8UAO5iFw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.0.tgz", + "integrity": "sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==", "dev": true, "dependencies": { "chalk": "^2.1.0" } }, "node_modules/@changesets/parse": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.16.tgz", - "integrity": "sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.0.tgz", + "integrity": "sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "js-yaml": "^3.13.1" } }, "node_modules/@changesets/pre": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", - "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.0.tgz", + "integrity": "sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "node_modules/@changesets/read": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", - "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.0.tgz", + "integrity": "sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/git": "^2.0.0", - "@changesets/logger": "^0.0.5", - "@changesets/parse": "^0.3.16", - "@changesets/types": "^5.2.1", + "@changesets/git": "^3.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/parse": "^0.4.0", + "@changesets/types": "^6.0.0", "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" } }, "node_modules/@changesets/types": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", - "integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz", + "integrity": "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==", "dev": true }, "node_modules/@changesets/write": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.2.3.tgz", - "integrity": "sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.3.0.tgz", + "integrity": "sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" @@ -407,10 +429,26 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz", + "integrity": "sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", - "integrity": "sha512-YGSPnndkcLo4PmVl2tKatEn+0mlVMr3yEpOOT0BeMria87PhvoJb5dg5f5Ft9fbCVgtAz4pWMzZVgSEGpDAlww==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.10.tgz", + "integrity": "sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w==", "cpu": [ "arm" ], @@ -424,9 +462,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.7.tgz", - "integrity": "sha512-YEDcw5IT7hW3sFKZBkCAQaOCJQLONVcD4bOyTXMZz5fr66pTHnAet46XAtbXAkJRfIn2YVhdC6R9g4xa27jQ1w==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz", + "integrity": "sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q==", "cpu": [ "arm64" ], @@ -440,9 +478,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.7.tgz", - "integrity": "sha512-jhINx8DEjz68cChFvM72YzrqfwJuFbfvSxZAk4bebpngGfNNRm+zRl4rtT9oAX6N9b6gBcFaJHFew5Blf6CvUw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.10.tgz", + "integrity": "sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw==", "cpu": [ "x64" ], @@ -456,9 +494,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.7.tgz", - "integrity": "sha512-dr81gbmWN//3ZnBIm6YNCl4p3pjnabg1/ZVOgz2fJoUO1a3mq9WQ/1iuEluMs7mCL+Zwv7AY5e3g1hjXqQZ9Iw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz", + "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==", "cpu": [ "arm64" ], @@ -472,9 +510,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.7.tgz", - "integrity": "sha512-Lc0q5HouGlzQEwLkgEKnWcSazqr9l9OdV2HhVasWJzLKeOt0PLhHaUHuzb8s/UIya38DJDoUm74GToZ6Wc7NGQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz", + "integrity": "sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA==", "cpu": [ "x64" ], @@ -488,9 +526,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.7.tgz", - "integrity": "sha512-+y2YsUr0CxDFF7GWiegWjGtTUF6gac2zFasfFkRJPkMAuMy9O7+2EH550VlqVdpEEchWMynkdhC9ZjtnMiHImQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz", + "integrity": "sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg==", "cpu": [ "arm64" ], @@ -504,9 +542,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.7.tgz", - "integrity": "sha512-CdXOxIbIzPJmJhrpmJTLx+o35NoiKBIgOvmvT+jeSadYiWJn0vFKsl+0bSG/5lwjNHoIDEyMYc/GAPR9jxusTA==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz", + "integrity": "sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag==", "cpu": [ "x64" ], @@ -520,9 +558,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.7.tgz", - "integrity": "sha512-Y+SCmWxsJOdQtjcBxoacn/pGW9HDZpwsoof0ttL+2vGcHokFlfqV666JpfLCSP2xLxFpF1lj7T3Ox3sr95YXww==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz", + "integrity": "sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg==", "cpu": [ "arm" ], @@ -536,9 +574,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.7.tgz", - "integrity": "sha512-inHqdOVCkUhHNvuQPT1oCB7cWz9qQ/Cz46xmVe0b7UXcuIJU3166aqSunsqkgSGMtUCWOZw3+KMwI6otINuC9g==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz", + "integrity": "sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ==", "cpu": [ "arm64" ], @@ -552,9 +590,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.7.tgz", - "integrity": "sha512-2BbiL7nLS5ZO96bxTQkdO0euGZIUQEUXMTrqLxKUmk/Y5pmrWU84f+CMJpM8+EHaBPfFSPnomEaQiG/+Gmh61g==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz", + "integrity": "sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg==", "cpu": [ "ia32" ], @@ -568,9 +606,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.7.tgz", - "integrity": "sha512-BVFQla72KXv3yyTFCQXF7MORvpTo4uTA8FVFgmwVrqbB/4DsBFWilUm1i2Oq6zN36DOZKSVUTb16jbjedhfSHw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz", + "integrity": "sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA==", "cpu": [ "loong64" ], @@ -584,9 +622,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.7.tgz", - "integrity": "sha512-DzAYckIaK+pS31Q/rGpvUKu7M+5/t+jI+cdleDgUwbU7KdG2eC3SUbZHlo6Q4P1CfVKZ1lUERRFP8+q0ob9i2w==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz", + "integrity": "sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A==", "cpu": [ "mips64el" ], @@ -600,9 +638,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.7.tgz", - "integrity": "sha512-JQ1p0SmUteNdUaaiRtyS59GkkfTW0Edo+e0O2sihnY4FoZLz5glpWUQEKMSzMhA430ctkylkS7+vn8ziuhUugQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz", + "integrity": "sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ==", "cpu": [ "ppc64" ], @@ -616,9 +654,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.7.tgz", - "integrity": "sha512-xGwVJ7eGhkprY/nB7L7MXysHduqjpzUl40+XoYDGC4UPLbnG+gsyS1wQPJ9lFPcxYAaDXbdRXd1ACs9AE9lxuw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz", + "integrity": "sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA==", "cpu": [ "riscv64" ], @@ -632,9 +670,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.7.tgz", - "integrity": "sha512-U8Rhki5PVU0L0nvk+E8FjkV8r4Lh4hVEb9duR6Zl21eIEYEwXz8RScj4LZWA2i3V70V4UHVgiqMpszXvG0Yqhg==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz", + "integrity": "sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA==", "cpu": [ "s390x" ], @@ -648,9 +686,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.7.tgz", - "integrity": "sha512-ZYZopyLhm4mcoZXjFt25itRlocKlcazDVkB4AhioiL9hOWhDldU9n38g62fhOI4Pth6vp+Mrd5rFKxD0/S+7aQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz", + "integrity": "sha512-SpYNEqg/6pZYoc+1zLCjVOYvxfZVZj6w0KROZ3Fje/QrM3nfvT2llI+wmKSrWuX6wmZeTapbarvuNNK/qepSgA==", "cpu": [ "x64" ], @@ -664,9 +702,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.7.tgz", - "integrity": "sha512-/yfjlsYmT1O3cum3J6cmGG16Fd5tqKMcg5D+sBYLaOQExheAJhqr8xOAEIuLo8JYkevmjM5zFD9rVs3VBcsjtQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz", + "integrity": "sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q==", "cpu": [ "x64" ], @@ -680,9 +718,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.7.tgz", - "integrity": "sha512-MYDFyV0EW1cTP46IgUJ38OnEY5TaXxjoDmwiTXPjezahQgZd+j3T55Ht8/Q9YXBM0+T9HJygrSRGV5QNF/YVDQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz", + "integrity": "sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg==", "cpu": [ "x64" ], @@ -696,9 +734,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.7.tgz", - "integrity": "sha512-JcPvgzf2NN/y6X3UUSqP6jSS06V0DZAV/8q0PjsZyGSXsIGcG110XsdmuWiHM+pno7/mJF6fjH5/vhUz/vA9fw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz", + "integrity": "sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA==", "cpu": [ "x64" ], @@ -712,9 +750,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.7.tgz", - "integrity": "sha512-ZA0KSYti5w5toax5FpmfcAgu3ZNJxYSRm0AW/Dao5up0YV1hDVof1NvwLomjEN+3/GMtaWDI+CIyJOMTRSTdMw==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz", + "integrity": "sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw==", "cpu": [ "arm64" ], @@ -728,9 +766,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.7.tgz", - "integrity": "sha512-CTOnijBKc5Jpk6/W9hQMMvJnsSYRYgveN6O75DTACCY18RA2nqka8dTZR+x/JqXCRiKk84+5+bRKXUSbbwsS0A==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz", + "integrity": "sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ==", "cpu": [ "ia32" ], @@ -744,9 +782,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.7.tgz", - "integrity": "sha512-gRaP2sk6hc98N734luX4VpF318l3w+ofrtTu9j5L8EQXF+FzQKV6alCOHMVoJJHvVK/mGbwBXfOL1HETQu9IGQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz", + "integrity": "sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA==", "cpu": [ "x64" ], @@ -784,9 +822,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -825,9 +863,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1002,15 +1040,15 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.38.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.38.3.tgz", - "integrity": "sha512-xt9iYyC5f39281j77JTA9C3ISJpW1XWkCcnw+2vM78CPnro6KhPfwQdPDfwS5JCPNuq0grm8cMdPUOPvrchDWw==", + "version": "7.39.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.39.0.tgz", + "integrity": "sha512-PuXxzadgnvp+wdeZFPonssRAj/EW4Gm4s75TXzPk09h3wJ8RS3x7typf95B4vwZRrPTQBGopdUl+/vHvlPdAcg==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.28.2", + "@microsoft/api-extractor-model": "7.28.3", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.61.0", + "@rushstack/node-core-library": "3.62.0", "@rushstack/rig-package": "0.5.1", "@rushstack/ts-command-line": "4.17.1", "colors": "~1.2.1", @@ -1018,34 +1056,21 @@ "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "~5.0.4" + "typescript": "5.3.3" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.2.tgz", - "integrity": "sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.3.tgz", + "integrity": "sha512-wT/kB2oDbdZXITyDh2SQLzaWwTOFbV326fP0pUwNW00WeliARs0qjmXBWmGWardEzp2U3/axkO3Lboqun6vrig==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.61.0" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" + "@rushstack/node-core-library": "3.62.0" } }, "node_modules/@microsoft/tsdoc": { @@ -1135,15 +1160,15 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.23", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", - "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, "node_modules/@rollup/pluginutils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", - "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -1163,9 +1188,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.2.tgz", - "integrity": "sha512-ee7BudTwwrglFYSc3UnqInDDjCLWHKrFmGNi4aK7jlEyg4CyPa1DCMrZfsN1O13YT76UFEqXz2CoN7BCGpUlJw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz", + "integrity": "sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig==", "cpu": [ "arm" ], @@ -1176,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.2.tgz", - "integrity": "sha512-xOuhj9HHtn8128ir8veoQsBbAUBasDbHIBniYTEx02pAmu9EXL+ZjJqngnNEy6ZgZ4h1JwL33GMNu3yJL5Mzow==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.1.tgz", + "integrity": "sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ==", "cpu": [ "arm64" ], @@ -1189,9 +1214,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.2.tgz", - "integrity": "sha512-NTGJWoL8bKyqyWFn9/RzSv4hQ4wTbaAv0lHHRwf4OnpiiP4P8W0jiXbm8Nc5BCXKmWAwuvJY82mcIU2TayC20g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz", + "integrity": "sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==", "cpu": [ "arm64" ], @@ -1202,9 +1227,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.2.tgz", - "integrity": "sha512-hlKqj7bpPvU15sZo4za14u185lpMzdwWLMc9raMqPK4wywt0wR23y1CaVQ4oAFXat3b5/gmRntyfpwWTKl+vvA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.1.tgz", + "integrity": "sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og==", "cpu": [ "x64" ], @@ -1215,9 +1240,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.2.tgz", - "integrity": "sha512-7ZIZx8c3u+pfI0ohQsft/GywrXez0uR6dUP0JhBuCK3sFO5TfdLn/YApnVkvPxuTv3+YKPIZend9Mt7Cz6sS3Q==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.1.tgz", + "integrity": "sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q==", "cpu": [ "arm" ], @@ -1228,9 +1253,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.2.tgz", - "integrity": "sha512-7Pk/5mO11JW/cH+a8lL/i0ZxmRGrbpYqN0VwO2DHhU+SJWWOH2zE1RAcPaj8KqiwC8DCDIJOSxjV9+9lLb6aeA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.1.tgz", + "integrity": "sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q==", "cpu": [ "arm64" ], @@ -1241,9 +1266,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.2.tgz", - "integrity": "sha512-KrRnuG5phJx756e62wxvWH2e+TK84MP2IVuPwfge+GBvWqIUfVzFRn09TKruuQBXzZp52Vyma7FjMDkwlA9xpg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.1.tgz", + "integrity": "sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw==", "cpu": [ "arm64" ], @@ -1253,10 +1278,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.1.tgz", + "integrity": "sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.2.tgz", - "integrity": "sha512-My+53GasPa2D2tU5dXiyHYwrELAUouSfkNlZ3bUKpI7btaztO5vpALEs3mvFjM7aKTvEbc7GQckuXeXIDKQ0fg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz", + "integrity": "sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==", "cpu": [ "x64" ], @@ -1267,9 +1305,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.2.tgz", - "integrity": "sha512-/f0Q6Sc+Vw54Ws6N8fxaEe4R7at3b8pFyv+O/F2VaQ4hODUJcRUcCBJh6zuqtgQQt7w845VTkGLFgWZkP3tUoQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.1.tgz", + "integrity": "sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw==", "cpu": [ "x64" ], @@ -1280,9 +1318,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.2.tgz", - "integrity": "sha512-NCKuuZWLht6zj7s6EIFef4BxCRX1GMr83S2W4HPCA0RnJ4iHE4FS1695q6Ewoa6A9nFjJe1//yUu0kgBU07Edw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.1.tgz", + "integrity": "sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g==", "cpu": [ "arm64" ], @@ -1293,9 +1331,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.2.tgz", - "integrity": "sha512-J5zL3riR4AOyU/J3M/i4k/zZ8eP1yT+nTmAKztCXJtnI36jYH0eepvob22mAQ/kLwfsK2TB6dbyVY1F8c/0H5A==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.1.tgz", + "integrity": "sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg==", "cpu": [ "ia32" ], @@ -1306,9 +1344,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.2.tgz", - "integrity": "sha512-pL0RXRHuuGLhvs7ayX/SAHph1hrDPXOM5anyYUQXWJEENxw3nfHkzv8FfVlEVcLyKPAEgDRkd6RKZq2SMqS/yg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz", + "integrity": "sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==", "cpu": [ "x64" ], @@ -1319,9 +1357,9 @@ ] }, "node_modules/@rushstack/node-core-library": { - "version": "3.61.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.61.0.tgz", - "integrity": "sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==", + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.62.0.tgz", + "integrity": "sha512-88aJn2h8UpSvdwuDXBv1/v1heM6GnBf3RjEy6ZPP7UnzHNCqOHA2Ut+ScYUbXcqIdfew9JlTAe3g+cnX9xQ/Aw==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -1375,36 +1413,12 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, - "node_modules/@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", - "dev": true - }, - "node_modules/@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", - "dev": true, - "dependencies": { - "@types/chai": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/is-ci": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.4.tgz", - "integrity": "sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==", - "dev": true, - "dependencies": { - "ci-info": "^3.1.0" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1430,9 +1444,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", - "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1451,16 +1465,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", - "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz", + "integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/type-utils": "6.12.0", - "@typescript-eslint/utils": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/type-utils": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1486,15 +1500,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz", + "integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", "debug": "^4.3.4" }, "engines": { @@ -1514,13 +1528,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz", + "integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1531,13 +1545,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", - "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz", + "integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/utils": "6.15.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1558,9 +1572,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz", + "integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1571,13 +1585,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz", + "integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1598,17 +1612,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", - "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz", + "integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", "semver": "^7.5.4" }, "engines": { @@ -1623,12 +1637,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz", + "integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.15.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1646,38 +1660,40 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", - "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.1.0.tgz", + "integrity": "sha512-kHQRk70vTdXAyQY2C0vKOHPyQD/R6IUzcGdO4vCuyr4alE5Yg1+Sk2jSdjlIrTTXdcNEs+ReWVM09mmSFJpzyQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", "@bcoe/v8-coverage": "^0.2.3", - "istanbul-lib-coverage": "^3.2.0", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.1.5", - "magic-string": "^0.30.1", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.2", "picocolors": "^1.0.0", - "std-env": "^3.3.3", + "std-env": "^3.5.0", "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.1.0" + "v8-to-istanbul": "^9.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": ">=0.32.0 <1" + "vitest": "^1.0.0" } }, "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz", + "integrity": "sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==", "dev": true, "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", + "@vitest/spy": "1.1.0", + "@vitest/utils": "1.1.0", "chai": "^4.3.10" }, "funding": { @@ -1685,13 +1701,13 @@ } }, "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.0.tgz", + "integrity": "sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==", "dev": true, "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", + "@vitest/utils": "1.1.0", + "p-limit": "^5.0.0", "pathe": "^1.1.1" }, "funding": { @@ -1699,56 +1715,56 @@ } }, "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, "dependencies": { "yocto-queue": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.0.tgz", + "integrity": "sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==", "dev": true, "dependencies": { - "magic-string": "^0.30.1", + "magic-string": "^0.30.5", "pathe": "^1.1.1", - "pretty-format": "^29.5.0" + "pretty-format": "^29.7.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.0.tgz", + "integrity": "sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==", "dev": true, "dependencies": { - "tinyspy": "^2.1.1" + "tinyspy": "^2.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.34.6.tgz", - "integrity": "sha512-/fxnCwGC0Txmr3tF3BwAbo3v6U2SkBTGR9UB8zo0Ztlx0BTOXHucE0gDHY7SjwEktCOHatiGmli9kZD6gYSoWQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.1.0.tgz", + "integrity": "sha512-7yU1QRFBplz0xJqcgt+agcbrNFdBmLo8UUppdKkFmYx+Ih0+yMYQOyr7kOB+YoggJY/p5ZzXxdbiOz7NBX2y+w==", "dev": true, "dependencies": { - "@vitest/utils": "0.34.6", - "fast-glob": "^3.3.0", - "fflate": "^0.8.0", - "flatted": "^3.2.7", + "@vitest/utils": "1.1.0", + "fast-glob": "^3.3.2", + "fflate": "^0.8.1", + "flatted": "^3.2.9", "pathe": "^1.1.1", "picocolors": "^1.0.0", "sirv": "^2.0.3" @@ -1757,86 +1773,87 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": ">=0.30.1 <1" + "vitest": "^1.0.0" } }, "node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.0.tgz", + "integrity": "sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" + "diff-sequences": "^29.6.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@volar/language-core": { - "version": "1.10.10", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.10.10.tgz", - "integrity": "sha512-nsV1o3AZ5n5jaEAObrS3MWLBWaGwUj/vAsc15FVNIv+DbpizQRISg9wzygsHBr56ELRH8r4K75vkYNMtsSNNWw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", "dev": true, "dependencies": { - "@volar/source-map": "1.10.10" + "@volar/source-map": "1.11.1" } }, "node_modules/@volar/source-map": { - "version": "1.10.10", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.10.10.tgz", - "integrity": "sha512-GVKjLnifV4voJ9F0vhP56p4+F3WGf+gXlRtjFZsv6v3WxBTWU3ZVeaRaEHJmWrcv5LXmoYYpk/SC25BKemPRkg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", "dev": true, "dependencies": { "muggle-string": "^0.3.1" } }, "node_modules/@volar/typescript": { - "version": "1.10.10", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.10.10.tgz", - "integrity": "sha512-4a2r5bdUub2m+mYVnLu2wt59fuoYWe7nf0uXtGHU8QQ5LDNfzAR0wK7NgDiQ9rcl2WT3fxT2AA9AylAwFtj50A==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", "dev": true, "dependencies": { - "@volar/language-core": "1.10.10", + "@volar/language-core": "1.11.1", "path-browserify": "^1.0.1" } }, "node_modules/@vue/compiler-core": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", - "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.13.tgz", + "integrity": "sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.0", - "@vue/shared": "3.3.8", + "@babel/parser": "^7.23.5", + "@vue/shared": "3.3.13", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz", - "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz", + "integrity": "sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.3.8", - "@vue/shared": "3.3.8" + "@vue/compiler-core": "3.3.13", + "@vue/shared": "3.3.13" } }, "node_modules/@vue/language-core": { - "version": "1.8.22", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.22.tgz", - "integrity": "sha512-bsMoJzCrXZqGsxawtUea1cLjUT9dZnDsy5TuZ+l1fxRMzUGQUG9+Ypq4w//CqpWmrx7nIAJpw2JVF/t258miRw==", + "version": "1.8.25", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.25.tgz", + "integrity": "sha512-NJk/5DnAZlpvXX8BdWmHI45bWGLViUaS3R/RMrmFSvFMSbJKuEODpM4kR0F0Ofv5SFzCWuNiMhxameWpVdQsnA==", "dev": true, "dependencies": { - "@volar/language-core": "~1.10.5", - "@volar/source-map": "~1.10.5", + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", "@vue/compiler-dom": "^3.3.0", "@vue/shared": "^3.3.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", "vue-template-compiler": "^2.7.14" }, "peerDependencies": { @@ -1873,9 +1890,9 @@ } }, "node_modules/@vue/shared": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz", - "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.13.tgz", + "integrity": "sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA==", "dev": true }, "node_modules/acorn": { @@ -1900,9 +1917,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "dev": true, "engines": { "node": ">=0.4.0" @@ -2831,9 +2848,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.7.tgz", - "integrity": "sha512-6brbTZVqxhqgbpqBR5MzErImcpA0SQdoKOkcWK/U30HtQxnokIpG3TX2r0IJqbFUzqLjhU/zC1S5ndgakObVCQ==", + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", + "integrity": "sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==", "dev": true, "hasInstallScript": true, "bin": { @@ -2843,28 +2860,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.19.7", - "@esbuild/android-arm64": "0.19.7", - "@esbuild/android-x64": "0.19.7", - "@esbuild/darwin-arm64": "0.19.7", - "@esbuild/darwin-x64": "0.19.7", - "@esbuild/freebsd-arm64": "0.19.7", - "@esbuild/freebsd-x64": "0.19.7", - "@esbuild/linux-arm": "0.19.7", - "@esbuild/linux-arm64": "0.19.7", - "@esbuild/linux-ia32": "0.19.7", - "@esbuild/linux-loong64": "0.19.7", - "@esbuild/linux-mips64el": "0.19.7", - "@esbuild/linux-ppc64": "0.19.7", - "@esbuild/linux-riscv64": "0.19.7", - "@esbuild/linux-s390x": "0.19.7", - "@esbuild/linux-x64": "0.19.7", - "@esbuild/netbsd-x64": "0.19.7", - "@esbuild/openbsd-x64": "0.19.7", - "@esbuild/sunos-x64": "0.19.7", - "@esbuild/win32-arm64": "0.19.7", - "@esbuild/win32-ia32": "0.19.7", - "@esbuild/win32-x64": "0.19.7" + "@esbuild/aix-ppc64": "0.19.10", + "@esbuild/android-arm": "0.19.10", + "@esbuild/android-arm64": "0.19.10", + "@esbuild/android-x64": "0.19.10", + "@esbuild/darwin-arm64": "0.19.10", + "@esbuild/darwin-x64": "0.19.10", + "@esbuild/freebsd-arm64": "0.19.10", + "@esbuild/freebsd-x64": "0.19.10", + "@esbuild/linux-arm": "0.19.10", + "@esbuild/linux-arm64": "0.19.10", + "@esbuild/linux-ia32": "0.19.10", + "@esbuild/linux-loong64": "0.19.10", + "@esbuild/linux-mips64el": "0.19.10", + "@esbuild/linux-ppc64": "0.19.10", + "@esbuild/linux-riscv64": "0.19.10", + "@esbuild/linux-s390x": "0.19.10", + "@esbuild/linux-x64": "0.19.10", + "@esbuild/netbsd-x64": "0.19.10", + "@esbuild/openbsd-x64": "0.19.10", + "@esbuild/sunos-x64": "0.19.10", + "@esbuild/win32-arm64": "0.19.10", + "@esbuild/win32-ia32": "0.19.10", + "@esbuild/win32-x64": "0.19.10" } }, "node_modules/escalade": { @@ -2886,15 +2904,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2953,9 +2971,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -2994,9 +3012,9 @@ } }, "node_modules/eslint-config-standard-with-typescript": { - "version": "40.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-40.0.0.tgz", - "integrity": "sha512-GXUJcwIXiTQaS3H4etv8a1lejVVdZYaxZNz3g7vt6GoJosQqMTurbmSC4FVGyHiGT/d1TjFr3+47A3xsHhsG+Q==", + "version": "43.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-43.0.0.tgz", + "integrity": "sha512-AT0qK01M5bmsWiE3UZvaQO5da1y1n6uQckAKqGNe6zPW5IOzgMLXZxw77nnFm+C11nxAZXsCPrbsgJhSrGfX6Q==", "dev": true, "dependencies": { "@typescript-eslint/parser": "^6.4.0", @@ -3058,9 +3076,9 @@ } }, "node_modules/eslint-plugin-es-x": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.4.0.tgz", - "integrity": "sha512-WJa3RhYzBtl8I37ebY9p76s61UhZyi4KaFOnX2A5r32RPazkXj5yoT6PGnD02dhwzEUj0KwsUdqfKDd/OuvGsw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", + "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", @@ -3078,9 +3096,9 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", - "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { "array-includes": "^3.1.7", @@ -3099,7 +3117,7 @@ "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -3139,14 +3157,14 @@ } }, "node_modules/eslint-plugin-n": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.1.tgz", - "integrity": "sha512-w46eDIkxQ2FaTHcey7G40eD+FhTXOdKudDXPUO2n9WNcslze/i/HT2qJ3GXjHngYSGDISIgPNhwGtgoix4zeOw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.5.0.tgz", + "integrity": "sha512-Hw02Bj1QrZIlKyj471Tb1jSReTl4ghIMHGuBGiMVmw+s0jOPbI4CBuYpGbZr+tdQ+VAvSK6FDSta3J4ib/SKHQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.1.0", + "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "ignore": "^5.2.4", "is-builtin-module": "^3.2.1", @@ -3166,9 +3184,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.0.tgz", + "integrity": "sha512-hQc+2zbnMeXcIkg+pKZtVa+3Yqx4WY7SMkn1PLZ4VbBEU7jJIpVn9347P8BBhTbz6ne85aXvQf30kvexcqBeWw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -3183,6 +3201,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", + "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3581,9 +3600,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -3847,9 +3866,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -4256,18 +4275,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -4784,10 +4791,14 @@ } }, "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, "engines": { "node": ">=14" }, @@ -4870,6 +4881,17 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.2.tgz", + "integrity": "sha512-Fjwkl6a0syt9TFN0JSYpOybxiMCkYNEeOTnOTNRbjphirLakznZXAqrXgj/7GG3D1dvETONNwrBfinvAbpunDg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", + "source-map-js": "^1.0.2" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5040,9 +5062,9 @@ } }, "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, "engines": { "node": ">=10" @@ -5171,13 +5193,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -5509,9 +5531,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "funding": [ { @@ -5528,7 +5550,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -5634,9 +5656,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5815,9 +5837,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/regexp.prototype.flags": { @@ -5913,9 +5935,9 @@ } }, "node_modules/rollup": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.5.2.tgz", - "integrity": "sha512-CRK1uoROBfkcqrZKyaFcqCcZWNsvJ6yVYZkqTlRocZhO2s5yER6Z3f/QaYtO8RGyloPnmhwgzuPQpNGeK210xQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz", + "integrity": "sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -5925,18 +5947,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.5.2", - "@rollup/rollup-android-arm64": "4.5.2", - "@rollup/rollup-darwin-arm64": "4.5.2", - "@rollup/rollup-darwin-x64": "4.5.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.5.2", - "@rollup/rollup-linux-arm64-gnu": "4.5.2", - "@rollup/rollup-linux-arm64-musl": "4.5.2", - "@rollup/rollup-linux-x64-gnu": "4.5.2", - "@rollup/rollup-linux-x64-musl": "4.5.2", - "@rollup/rollup-win32-arm64-msvc": "4.5.2", - "@rollup/rollup-win32-ia32-msvc": "4.5.2", - "@rollup/rollup-win32-x64-msvc": "4.5.2", + "@rollup/rollup-android-arm-eabi": "4.9.1", + "@rollup/rollup-android-arm64": "4.9.1", + "@rollup/rollup-darwin-arm64": "4.9.1", + "@rollup/rollup-darwin-x64": "4.9.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.1", + "@rollup/rollup-linux-arm64-gnu": "4.9.1", + "@rollup/rollup-linux-arm64-musl": "4.9.1", + "@rollup/rollup-linux-riscv64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-gnu": "4.9.1", + "@rollup/rollup-linux-x64-musl": "4.9.1", + "@rollup/rollup-win32-arm64-msvc": "4.9.1", + "@rollup/rollup-win32-ia32-msvc": "4.9.1", + "@rollup/rollup-win32-x64-msvc": "4.9.1", "fsevents": "~2.3.2" } }, @@ -6203,13 +6226,13 @@ "dev": true }, "node_modules/sirv": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", - "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", "totalist": "^3.0.0" }, "engines": { @@ -6464,9 +6487,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.6.0.tgz", + "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "dev": true }, "node_modules/stream-transform": { @@ -6640,13 +6663,13 @@ } }, "node_modules/synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", + "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", "dev": true, "dependencies": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" + "@pkgr/utils": "^2.4.2", + "tslib": "^2.6.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6694,9 +6717,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", + "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6735,6 +6758,15 @@ "node": ">=0.6.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6784,9 +6816,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -6991,9 +7023,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7091,13 +7123,13 @@ } }, "node_modules/vite": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.2.tgz", - "integrity": "sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", + "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==", "dev": true, "dependencies": { "esbuild": "^0.19.3", - "postcss": "^8.4.31", + "postcss": "^8.4.32", "rollup": "^4.2.0" }, "bin": { @@ -7146,32 +7178,31 @@ } }, "node_modules/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.0.tgz", + "integrity": "sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==", "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vite-plugin-dts": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.6.3.tgz", - "integrity": "sha512-NyRvgobl15rYj65coi/gH7UAEH+CpSjh539DbGb40DfOTZSvDLNYTzc8CK4460W+LqXuMK7+U3JAxRB3ksrNPw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.6.4.tgz", + "integrity": "sha512-yOVhUI/kQhtS6lCXRYYLv2UUf9bftcwQK9ROxCX2ul17poLQs02ctWX7+vXB8GPRzH8VCK3jebEFtPqqijXx6w==", "dev": true, "dependencies": { "@microsoft/api-extractor": "^7.38.0", @@ -7195,59 +7226,57 @@ } }, "node_modules/vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.0.tgz", + "integrity": "sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.1.0", + "@vitest/runner": "1.1.0", + "@vitest/snapshot": "1.1.0", + "@vitest/spy": "1.1.0", + "@vitest/utils": "1.1.0", + "acorn-walk": "^8.3.0", "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", + "std-env": "^3.5.0", + "strip-literal": "^1.3.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.1", + "vite": "^5.0.0", + "vite-node": "1.1.0", "why-is-node-running": "^2.2.2" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "^1.0.0", + "@vitest/ui": "^1.0.0", "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@types/node": { + "optional": true + }, "@vitest/browser": { "optional": true }, @@ -7259,18 +7288,65 @@ }, "jsdom": { "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true } } }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/vue-template-compiler": { "version": "2.7.15", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz", @@ -7282,13 +7358,13 @@ } }, "node_modules/vue-tsc": { - "version": "1.8.22", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.22.tgz", - "integrity": "sha512-j9P4kHtW6eEE08aS5McFZE/ivmipXy0JzrnTgbomfABMaVKx37kNBw//irL3+LlE3kOo63XpnRigyPC3w7+z+A==", + "version": "1.8.25", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.25.tgz", + "integrity": "sha512-lHsRhDc/Y7LINvYhZ3pv4elflFADoEOo67vfClAfF2heVHpHmVquLSjojgCSIwzA4F0Pc4vowT/psXCYcfk+iQ==", "dev": true, "dependencies": { - "@volar/typescript": "~1.10.5", - "@vue/language-core": "1.8.22", + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.25", "semver": "^7.5.4" }, "bin": { diff --git a/package.json b/package.json index d3785ca..1264c77 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "engines": { "node": ">=20.10.0", - "npm": ">=10.2.3" + "npm": ">=10.2.0" }, "scripts": { "clean": "rm -rf coverage && rm -rf dist", @@ -49,7 +49,7 @@ "lint:fix": "npm run lint -- --fix", "format": "prettier --check src/", "format:fix": "prettier --write src/", - "test": "vitest --no-threads", + "test": "vitest --pool forks", "test:ui": "vitest --ui", "coverage": "vitest --run --coverage", "report": "changeset", @@ -59,25 +59,25 @@ "emitten": "^0.6.1" }, "devDependencies": { - "@changesets/changelog-github": "^0.4.8", - "@changesets/cli": "^2.26.2", - "@types/node": "^20.10.0", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@vitest/coverage-v8": "^0.34.6", - "@vitest/ui": "^0.34.6", - "eslint": "^8.54.0", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^40.0.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-n": "^16.3.1", - "eslint-plugin-prettier": "^5.0.1", + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.1", + "@types/node": "^20.10.5", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@vitest/coverage-v8": "^1.1.0", + "@vitest/ui": "^1.1.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "^43.0.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.5.0", + "eslint-plugin-prettier": "^5.1.0", "eslint-plugin-promise": "^6.1.1", "happy-dom": "^12.10.3", - "prettier": "^3.1.0", - "typescript": "^5.3.2", - "vite": "^5.0.2", - "vite-plugin-dts": "^3.6.3", - "vitest": "^0.34.6" + "prettier": "^3.1.1", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vite-plugin-dts": "^3.6.4", + "vitest": "^1.1.0" }, "peerDependencies": { "emitten": "^0.6.1" diff --git a/src/Earwurm.ts b/src/Earwurm.ts index d55037f..e557f9d 100644 --- a/src/Earwurm.ts +++ b/src/Earwurm.ts @@ -1,7 +1,7 @@ import {EmittenCommon} from 'emitten'; -import {getErrorMessage, unlockAudioContext} from './helpers'; -import {clamp, msToSec, secToMs} from './utilities'; +import {getErrorMessage, linearRamp, unlockAudioContext} from './helpers'; +import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities'; import {tokens} from './tokens'; import type { @@ -10,7 +10,7 @@ import type { ManagerEventMap, ManagerConfig, LibraryEntry, - LibraryKeys, + StackId, StackEventMap, } from './types'; @@ -20,9 +20,14 @@ export class Earwurm extends EmittenCommon { static readonly maxStackSize = tokens.maxStackSize; static readonly suspendAfterMs = tokens.suspendAfterMs; + static readonly errorMessage = { + close: 'Failed to close the Earwurm AudioContext.', + resume: 'Failed to resume the Earwurm AudioContext.', + }; + private _volume = 1; private _mute = false; - private _keys: LibraryKeys = []; + private _keys: StackId[] = []; private _state: ManagerState = 'suspended'; readonly #context = new AudioContext(); @@ -59,19 +64,22 @@ export class Earwurm extends EmittenCommon { set volume(value: number) { const oldVolume = this._volume; - const newVolume = clamp({preference: value, min: 0, max: 1}); + const newVolume = clamp(0, value, 1); this._volume = newVolume; + if (oldVolume !== newVolume) { + this.emit('volume', newVolume); + } + if (this._mute) return; - this.#gainNode.gain - .cancelScheduledValues(this.#context.currentTime) - .setValueAtTime(oldVolume, this.#context.currentTime) - .linearRampToValueAtTime( - newVolume, - this.#context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.#context; + linearRamp( + this.#gainNode.gain, + {from: oldVolume, to: newVolume}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); } get mute() { @@ -79,18 +87,21 @@ export class Earwurm extends EmittenCommon { } set mute(value: boolean) { + if (this._mute !== value) { + this.emit('mute', value); + } + this._mute = value; const fromValue = value ? this._volume : 0; const toValue = value ? 0 : this._volume; - this.#gainNode.gain - .cancelScheduledValues(this.#context.currentTime) - .setValueAtTime(fromValue, this.#context.currentTime) - .linearRampToValueAtTime( - toValue, - this.#context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.#context; + linearRamp( + this.#gainNode.gain, + {from: fromValue, to: toValue}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); } get unlocked() { @@ -123,9 +134,14 @@ export class Earwurm extends EmittenCommon { } add(...entries: LibraryEntry[]) { - const newKeys: LibraryKeys = []; + const newKeys: StackId[] = []; + + const newStacks = entries.reduce((collection, {id, path}) => { + const existingStack = this.get(id); + const identicalStack = existingStack?.path === path; + + if (identicalStack) return collection; - const newStacks = entries.map(({id, path}) => { newKeys.push(id); const newStack = new Stack(id, path, this.#context, this.#gainNode, { @@ -133,25 +149,25 @@ export class Earwurm extends EmittenCommon { request: this.#request, }); - newStack.on('statechange', this.#handleStackStateChange); + newStack.on('state', this.#handleStackState); - return newStack; - }); + return [...collection, newStack]; + }, []); - const replacedKeys = this.#library.reduce( + const replacedKeys = this.#library.reduce( (collection, {id}) => newKeys.includes(id) ? [...collection, id] : collection, [], ); - this.remove(...replacedKeys); + if (replacedKeys.length) this.remove(...replacedKeys); this.#setLibrary([...this.#library, ...newStacks]); return newKeys; } - remove(...ids: LibraryKeys) { - const removedKeys: LibraryKeys = []; + remove(...ids: StackId[]) { + const removedKeys: StackId[] = []; const filteredLibrary = this.#library.filter((stack) => { const match = ids.includes(stack.id); @@ -190,7 +206,7 @@ export class Earwurm extends EmittenCommon { }) .catch((error) => { this.emit('error', [ - 'Failed to close the Earwurm AudioContext.', + Earwurm.errorMessage.close, getErrorMessage(error), ]); }); @@ -223,7 +239,7 @@ export class Earwurm extends EmittenCommon { if (this._state === 'suspended' || this._state === 'interrupted') { this.#context.resume().catch((error) => { this.emit('error', [ - 'Failed to resume the Earwurm AudioContext.', + Earwurm.errorMessage.resume, getErrorMessage(error), ]); }); @@ -242,15 +258,23 @@ export class Earwurm extends EmittenCommon { } #setLibrary(library: Stack[]) { + const oldKeys = [...this._keys]; + const newKeys = library.map(({id}) => id); + const identicalKeys = arrayShallowEquals(oldKeys, newKeys); + this.#library = library; - this._keys = this.#library.map(({id}) => id); + this._keys = newKeys; + + if (!identicalKeys) { + this.emit('library', newKeys, oldKeys); + } } #setState(value: ManagerState) { if (this._state === value) return; this._state = value; - this.emit('statechange', value); + this.emit('state', value); if (value === 'running') { this._unlocked = true; @@ -290,11 +314,11 @@ export class Earwurm extends EmittenCommon { this.#setState(this.#context.state); }; - readonly #handleStackStateChange: StackEventMap['statechange'] = (state) => { + readonly #handleStackState: StackEventMap['state'] = (current) => { // We don't care about re-setting the auto-suspension each time // a new `Sound` is prepared... but it will do that anyways // since `Stack` returns to `idle` once loaded. - if (state === 'loading') return; + if (current === 'loading') return; if (this.playing) { this.#autoResume(); diff --git a/src/Sound.ts b/src/Sound.ts index b319d76..39823e6 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -1,20 +1,36 @@ import {EmittenCommon} from 'emitten'; -import {clamp, msToSec} from './utilities'; +import {linearRamp} from './helpers'; +import {clamp, msToSec, progressPercentage} from './utilities'; import {tokens} from './tokens'; -import type {SoundId, SoundState, SoundEventMap, SoundConfig} from './types'; +import type { + SoundId, + SoundState, + SoundEventMap, + SoundProgressEvent, + SoundConfig, +} from './types'; export class Sound extends EmittenCommon { - // "Readonly accessor" properties private _volume = 1; private _mute = false; + private _speed = 1; private _state: SoundState = 'created'; - // "True private" properties readonly #source: AudioBufferSourceNode; readonly #gainNode: GainNode; readonly #fadeSec: number = 0; - #started = false; + readonly #progress = { + elapsed: 0, + remaining: 0, + percentage: 0, + iterations: 0, + }; + + #intervalId = 0; + #timestamp = 0; + #elapsedSnapshot = 0; + #hasStarted = false; constructor( readonly id: SoundId, @@ -34,31 +50,39 @@ export class Sound extends EmittenCommon { this.#source.connect(this.#gainNode).connect(this.destination); this.#gainNode.gain.setValueAtTime(this._volume, this.context.currentTime); + this.#progress.remaining = this.#source.buffer.duration; - // We could `emit` a "created" event, but it wouldn't get caught - // by any listeners, since those cannot be attached until after creation. + // The `ended` event is fired either when the sound has played its full duration, + // or the `.stop()` method has been called. this.#source.addEventListener('ended', this.#handleEnded, {once: true}); } + private get hasProgressSub() { + return this.activeEvents.some((event) => event === 'progress'); + } + get volume() { return this._volume; } set volume(value: number) { const oldVolume = this._volume; - const newVolume = clamp({preference: value, min: 0, max: 1}); + const newVolume = clamp(0, value, 1); this._volume = newVolume; + if (oldVolume !== newVolume) { + this.emit('volume', newVolume); + } + if (this._mute) return; - this.#gainNode.gain - .cancelScheduledValues(this.context.currentTime) - .setValueAtTime(oldVolume, this.context.currentTime) - .linearRampToValueAtTime( - newVolume, - this.context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.context; + linearRamp( + this.#gainNode.gain, + {from: oldVolume, to: newVolume}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); } get mute() { @@ -66,18 +90,60 @@ export class Sound extends EmittenCommon { } set mute(value: boolean) { + if (this._mute !== value) { + this.emit('mute', value); + } + this._mute = value; const fromValue = value ? this._volume : 0; const toValue = value ? 0 : this._volume; - this.#gainNode.gain - .cancelScheduledValues(this.context.currentTime) - .setValueAtTime(fromValue, this.context.currentTime) - .linearRampToValueAtTime( - toValue, - this.context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.context; + linearRamp( + this.#gainNode.gain, + {from: fromValue, to: toValue}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); + } + + get speed() { + return this._speed; + } + + set speed(value: number) { + const oldSpeed = this._speed; + const newSpeed = clamp(tokens.minSpeed, value, tokens.maxSpeed); + + if (oldSpeed === newSpeed) return; + + this._speed = newSpeed; + this.emit('speed', newSpeed); + + // Must return if `paused`, because the way we currently + // "pause" is by slowing `playbackRate` to a halt. + if (this._state === 'paused') return; + + if (this._state !== 'playing') { + this.#source.playbackRate.value = newSpeed; + return; + } + + const {currentTime} = this.context; + this.#timestamp = Math.max(currentTime, tokens.minStartTime); + this.#elapsedSnapshot = this.#progress.elapsed; + + // TODO: Not transitioning to new `speed` for now... + // this will be complicated given our `progress` calculations. + /* + linearRamp( + this.#source.playbackRate, + {from: oldSpeed, to: newSpeed}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); + */ + + this.#source.playbackRate.value = newSpeed; } get loop() { @@ -92,46 +158,80 @@ export class Sound extends EmittenCommon { return this.#source.buffer?.duration ?? 0; } + get progress(): SoundProgressEvent { + if (!this.#hasStarted) return {...this.#progress}; + + // When combining `speed + pause + looping`, we can end up with + // an accumulative loss of precision. The `progress` calculations + // can end up behind the actual play position of the sound. + // Not yet sure how to resolve this. + this.#incrementLoop(); + + const timeSince = + Math.max(this.context.currentTime - this.#timestamp, 0) * this.speed; + + this.#progress.elapsed = clamp( + 0, + this.#elapsedSnapshot + timeSince, + this.duration, + ); + this.#progress.remaining = this.duration - this.#progress.elapsed; + this.#progress.percentage = clamp( + 0, + progressPercentage(this.#progress.elapsed, this.duration), + 100, + ); + + return {...this.#progress}; + } + get state() { return this._state; } play() { - if (!this.#started) { + if (!this.#hasStarted) { this.#source.start(); - this.#started = true; + this.#hasStarted = true; } if (this._state === 'paused') { - this.#source.playbackRate.value = 1; - this.mute = false; + // Restoring directly to `playbackRate` instead of `speed`. + this.#source.playbackRate.value = this._speed; } + this.#timestamp = Math.max(this.context.currentTime, tokens.minStartTime); + this.#elapsedSnapshot = this.#progress.elapsed; + this.#setState('playing'); return this; } pause() { - if (this._state === 'paused') return this; - // There is no `pause/resume` API for a `AudioBufferSourceNode`. - // Lowering the `playbackRate` isn't ideal as technically the - // audio is still playing in the background and using resources. + // To solve this, we leverage `playbackRate`. // https://github.com/WebAudio/web-audio-api-v2/issues/105 - this.#source.playbackRate.value = tokens.minPlaybackRate; - this.mute = true; + if (this._state !== 'playing') return this; + // Directly setting `playbackRate` instead of `speed`, + // as we do not want to trigger an `event` or `ramp`. + this.#source.playbackRate.value = tokens.pauseSpeed; this.#setState('paused'); + // TODO: We will need to "fade to silent" if using + // `transitions`... but not `trigger` a volume event. return this; } stop() { + // This state is useful to distinguish between + // an explicit "stop" and a natural "end". this.#setState('stopping'); - this.#source.stop(); - this.#source.disconnect(); - this.empty(); + + if (this.#hasStarted) this.#source.stop(); + // Required to manually emit the `ended` event for "un-started" sounds. + else this.#handleEnded(); return this; } @@ -140,12 +240,64 @@ export class Sound extends EmittenCommon { if (this._state === value) return; this._state = value; - this.emit('statechange', value); + this.emit('state', value); + + if (value === 'playing') { + this.#intervalId = this.hasProgressSub + ? requestAnimationFrame(this.#handleInterval) + : 0; + } else { + cancelAnimationFrame(this.#intervalId); + this.#intervalId = 0; + + // TODO: We may not get a final `100%` value, as + // `ended > empty()` might be clearing subscriptions + // before they get a chance to execute one last time. + if (this.hasProgressSub) this.#updateProgress(); + } + } + + #incrementLoop() { + if (!this.loop) return; + + if ( + this.#progress.elapsed === this.duration || + this.#progress.remaining === 0 || + this.#progress.percentage === 100 + ) { + this.#progress.elapsed = 0; + this.#progress.remaining = this.duration; + this.#progress.percentage = 0; + this.#progress.iterations++; + + this.#timestamp = this.context.currentTime; + this.#elapsedSnapshot = 0; + } + } + + #updateProgress() { + this.emit('progress', this.progress); } + readonly #handleInterval = (_timestamp = 0) => { + this.#updateProgress(); + // Recursive call allows for a loop per-animation-frame. + this.#intervalId = requestAnimationFrame(this.#handleInterval); + }; + readonly #handleEnded = () => { - // Intentionally not setting `stopping` state here, - // but we may want ot consider a "ending" state instead. - this.emit('ended', {id: this.id, source: this.#source}); + this.#setState('ending'); + + this.emit('ended', { + id: this.id, + source: this.#source, + neverStarted: !this.#hasStarted, + }); + + // This needs to happen AFTER our artifical `ended` event is emitted. + // Otherwise, the native `ended` event may not be called in time + // after triggering `source.stop()`. + this.#source.disconnect(); + this.empty(); }; } diff --git a/src/Stack.ts b/src/Stack.ts index e12498c..67e8f90 100644 --- a/src/Stack.ts +++ b/src/Stack.ts @@ -1,7 +1,12 @@ import {EmittenCommon} from 'emitten'; -import {getErrorMessage, fetchAudioBuffer, scratchBuffer} from './helpers'; -import {clamp, msToSec, secToMs} from './utilities'; +import { + getErrorMessage, + fetchAudioBuffer, + linearRamp, + scratchBuffer, +} from './helpers'; +import {arrayShallowEquals, clamp, msToSec, secToMs} from './utilities'; import {tokens} from './tokens'; import type { @@ -35,8 +40,9 @@ export class Stack extends EmittenCommon { readonly #gainNode: GainNode; readonly #fadeSec: number = 0; - #totalSoundsCreated = 0; readonly #request: StackConfig['request']; + + #totalSoundsCreated = 0; #queue: Sound[] = []; constructor( @@ -64,19 +70,22 @@ export class Stack extends EmittenCommon { set volume(value: number) { const oldVolume = this._volume; - const newVolume = clamp({preference: value, min: 0, max: 1}); + const newVolume = clamp(0, value, 1); this._volume = newVolume; + if (oldVolume !== newVolume) { + this.emit('volume', newVolume); + } + if (this._mute) return; - this.#gainNode.gain - .cancelScheduledValues(this.context.currentTime) - .setValueAtTime(oldVolume, this.context.currentTime) - .linearRampToValueAtTime( - newVolume, - this.context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.context; + linearRamp( + this.#gainNode.gain, + {from: oldVolume, to: newVolume}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); } get mute() { @@ -84,18 +93,21 @@ export class Stack extends EmittenCommon { } set mute(value: boolean) { + if (this._mute !== value) { + this.emit('mute', value); + } + this._mute = value; const fromValue = value ? this._volume : 0; const toValue = value ? 0 : this._volume; - this.#gainNode.gain - .cancelScheduledValues(this.context.currentTime) - .setValueAtTime(fromValue, this.context.currentTime) - .linearRampToValueAtTime( - toValue, - this.context.currentTime + this.#fadeSec, - ); + const {currentTime} = this.context; + linearRamp( + this.#gainNode.gain, + {from: fromValue, to: toValue}, + {from: currentTime, to: currentTime + this.#fadeSec}, + ); } get keys() { @@ -176,7 +188,7 @@ export class Stack extends EmittenCommon { fadeMs: secToMs(this.#fadeSec), }); - newSound.on('statechange', this.#handleSoundState); + newSound.on('state', this.#handleSoundState); newSound.once('ended', this.#handleSoundEnded); // We do not filter out identical `id` values, @@ -193,40 +205,44 @@ export class Stack extends EmittenCommon { ({id}) => !outOfBoundsIds.includes(id), ); - outOfBounds.forEach((expiredSound) => { - expiredSound.stop(); - }); - + outOfBounds.forEach((expiredSound) => expiredSound.stop()); this.#setQueue(filteredQueue); return newSound; } #setQueue(value: Sound[]) { + const oldKeys = [...this._keys]; + const newKeys = value.map(({id}) => id); + const identicalKeys = arrayShallowEquals(oldKeys, newKeys); + this.#queue = value; - this._keys = value.map(({id}) => id); + this._keys = newKeys; + + if (!identicalKeys) { + this.emit('queue', newKeys, oldKeys); + } } #setState(value: StackState) { if (this._state === value) return; this._state = value; - this.emit('statechange', value); + this.emit('state', value); } readonly #handleStateFromQueue = () => { this.#setState(this.playing ? 'playing' : 'idle'); }; - readonly #handleSoundState: SoundEventMap['statechange'] = (_state) => { + readonly #handleSoundState: SoundEventMap['state'] = (_current) => { this.#handleStateFromQueue(); }; readonly #handleSoundEnded: SoundEventMap['ended'] = (event) => { this.#setQueue(this.#queue.filter(({id}) => id !== event.id)); - // We only set `stopping` state when `.stop()` is called. - // There is no `statechange` specifically for "ended". + // There is an `ending` value, but it is redundant with the `ended` event. this.#handleStateFromQueue(); }; } diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 8e4cda4..9256005 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,4 +1,5 @@ export {getErrorMessage} from './error-message'; export {fetchAudioBuffer} from './fetch-audio-buffer'; +export {linearRamp} from './linear-ramp'; export {scratchBuffer} from './scratch-buffer'; export {unlockAudioContext} from './unlock-audio-context'; diff --git a/src/helpers/linear-ramp.ts b/src/helpers/linear-ramp.ts new file mode 100644 index 0000000..66bc7fb --- /dev/null +++ b/src/helpers/linear-ramp.ts @@ -0,0 +1,15 @@ +interface LinearRampValue { + from: number; + to: number; +} + +export function linearRamp( + param: AudioParam, + value: LinearRampValue, + time: LinearRampValue, +) { + return param + .cancelScheduledValues(time.from) + .setValueAtTime(value.from, time.from) + .linearRampToValueAtTime(value.to, time.to); +} diff --git a/src/index.ts b/src/index.ts index 6411f27..9f6b9ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,16 +2,19 @@ export {Earwurm} from './Earwurm'; export {Stack} from './Stack'; export {Sound} from './Sound'; +export {tokens} from './tokens'; export type { ManagerState, ManagerEventMap, ManagerConfig, LibraryEntry, - LibraryKeys, + StackId, StackState, StackEventMap, StackConfig, + SoundId, SoundState, SoundEventMap, + SoundProgressEvent, SoundConfig, } from './types'; diff --git a/src/tests/Abstract.test.ts b/src/tests/Abstract.test.ts index aa6f85a..72328f8 100644 --- a/src/tests/Abstract.test.ts +++ b/src/tests/Abstract.test.ts @@ -2,7 +2,7 @@ import {afterEach, describe, it, expect, vi} from 'vitest'; import {msToSec} from '../utilities'; import {Sound} from '../Sound'; -import type {SoundConfig} from '../types'; +import type {SoundConfig, SoundEventMap} from '../types'; // This test covers any shared implementation between // each component. Eventually, I will create a proper @@ -30,7 +30,7 @@ describe('Abstract implementation', () => { mockSound.mute = false; }); - it('allows `set` and `get`', () => { + it('allows `set` and `get`', async () => { expect(mockSound.mute).toBe(false); mockSound.mute = true; expect(mockSound.mute).toBe(true); @@ -40,7 +40,7 @@ describe('Abstract implementation', () => { it.todo('sets gain to 0 when muted'); it.todo('sets gain to volume when un-muted'); - it('fades to new value', () => { + it('fades to new value', async () => { const mockOptions: SoundConfig = { volume: 0.6, fadeMs: 100, @@ -69,6 +69,27 @@ describe('Abstract implementation', () => { // `gain.value` is `0`. expect(spyGainRamp).toBeCalledWith(0, endTime); }); + + it('triggers mute event when set to a unique value', async () => { + const spyMute: SoundEventMap['mute'] = vi.fn((_muted) => {}); + + mockSound.on('mute', spyMute); + expect(spyMute).not.toBeCalled(); + + mockSound.mute = false; + expect(spyMute).not.toBeCalled(); + + mockSound.mute = true; + expect(spyMute).toBeCalledWith(true); + + mockSound.mute = false; + expect(spyMute).toBeCalledWith(false); + + mockSound.off('mute', spyMute); + + mockSound.mute = true; + expect(spyMute).not.toHaveBeenLastCalledWith(true); + }); }); describe('volume', () => { @@ -84,24 +105,24 @@ describe('Abstract implementation', () => { mockSound.mute = false; }); - it('allows `set` and `get`', () => { + it('allows `set` and `get`', async () => { const mockVolume = 0.4; mockSound.volume = mockVolume; expect(mockSound.volume).toBe(mockVolume); }); - it('restricts value to a minimum of `0`', () => { + it('restricts value to a minimum of `0`', async () => { mockSound.volume = -2; expect(mockSound.volume).toBe(0); }); - it('restricts value to a maximum of `1`', () => { + it('restricts value to a maximum of `1`', async () => { mockSound.volume = 2; expect(mockSound.volume).toBe(1); }); - it('sets value on `gain`', () => { + it('sets value on `gain`', async () => { const oldValue = mockSound.volume; const newValue = 0.6; const {currentTime} = defaultContext; @@ -124,7 +145,7 @@ describe('Abstract implementation', () => { expect(spyGainRamp).toBeCalledWith(newValue, currentTime); }); - it('does not set value on gain if muted', () => { + it('does not set value on gain if muted', async () => { const mockVolume = 0.2; const spyGainCancel = vi.spyOn( @@ -158,7 +179,7 @@ describe('Abstract implementation', () => { expect(spyGainRamp).not.toBeCalledTimes(2); }); - it('fades to new volume', () => { + it('fades to new volume', async () => { const mockOptions: SoundConfig = { volume: 0.6, fadeMs: 100, @@ -188,5 +209,31 @@ describe('Abstract implementation', () => { // `gain.value` is `newValue`. expect(spyGainRamp).toBeCalledWith(newValue, endTime); }); + + it('triggers volume event when set to a unique value (regardless of mute state)', async () => { + const spyVolume: SoundEventMap['volume'] = vi.fn((_level) => {}); + + mockSound.on('volume', spyVolume); + expect(spyVolume).not.toBeCalled(); + + mockSound.volume = 1; + expect(spyVolume).not.toBeCalled(); + + mockSound.mute = true; + mockSound.volume = 0.8; + expect(spyVolume).toBeCalledWith(0.8); + + mockSound.mute = false; + mockSound.volume = 0; + expect(spyVolume).toBeCalledWith(0); + + mockSound.volume = 1; + expect(spyVolume).toBeCalledWith(1); + + mockSound.off('volume', spyVolume); + + mockSound.volume = 0.6; + expect(spyVolume).not.toBeCalledWith(0.6); + }); }); }); diff --git a/src/tests/Earwurm.test.ts b/src/tests/Earwurm.test.ts index c304a0e..18619e9 100644 --- a/src/tests/Earwurm.test.ts +++ b/src/tests/Earwurm.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, vi} from 'vitest'; +import {afterEach, describe, it, expect, vi} from 'vitest'; import {Earwurm} from '../Earwurm'; import {Stack} from '../Stack'; @@ -8,34 +8,40 @@ import type { ManagerEventMap, ManagerConfig, LibraryEntry, - LibraryKeys, + StackId, } from '../types'; import {mockData} from './mock'; describe('Earwurm component', () => { + let mockManager = new Earwurm(); + const mockEntries: LibraryEntry[] = [ {id: 'Zero', path: mockData.audio}, {id: 'One', path: 'to/no/file.mp3'}, {id: 'Two', path: ''}, ]; + const mockInitialKeys: StackId[] = mockEntries.map(({id}) => id); - describe('initialization', () => { - const testManager = new Earwurm(); + afterEach(() => { + mockManager.teardown(); + mockManager = new Earwurm(); + }); - it('is initialized with default values', () => { - expect(testManager).toBeInstanceOf(Earwurm); + describe('initialization', () => { + it('is initialized with default values', async () => { + expect(mockManager).toBeInstanceOf(Earwurm); // Class static properties expect(Earwurm).toHaveProperty('maxStackSize', tokens.maxStackSize); expect(Earwurm).toHaveProperty('suspendAfterMs', tokens.suspendAfterMs); // Instance properties - expect(testManager).toHaveProperty('volume', 1); - expect(testManager).toHaveProperty('mute', false); - expect(testManager).toHaveProperty('unlocked', false); - expect(testManager).toHaveProperty('keys', []); - expect(testManager).toHaveProperty('state', 'suspended'); - expect(testManager).toHaveProperty('playing', false); + expect(mockManager).toHaveProperty('volume', 1); + expect(mockManager).toHaveProperty('mute', false); + expect(mockManager).toHaveProperty('unlocked', false); + expect(mockManager).toHaveProperty('keys', []); + expect(mockManager).toHaveProperty('state', 'suspended'); + expect(mockManager).toHaveProperty('playing', false); }); }); @@ -50,30 +56,27 @@ describe('Earwurm component', () => { describe('keys', () => { it('contains ids of each active Stack', async () => { - const testManager = new Earwurm(); + expect(mockManager.keys).toHaveLength(0); + mockManager.add(...mockEntries); - expect(testManager.keys).toHaveLength(0); - testManager.add(...mockEntries); - - expect(testManager.keys).toStrictEqual(['Zero', 'One', 'Two']); - testManager.remove('Zero'); - expect(testManager.keys).toStrictEqual(['One', 'Two']); + expect(mockManager.keys).toStrictEqual(['Zero', 'One', 'Two']); + mockManager.remove('Zero'); + expect(mockManager.keys).toStrictEqual(['One', 'Two']); }); }); describe('state', () => { const clickEvent = new Event('click'); - it('triggers `statechange` event for every state', async () => { - const testManager = new Earwurm(); - const spyState: ManagerEventMap['statechange'] = vi.fn((_state) => {}); + it('triggers `state` event for every state', async () => { + const spyState: ManagerEventMap['state'] = vi.fn((_current) => {}); - testManager.on('statechange', spyState); + mockManager.on('state', spyState); expect(spyState).not.toBeCalled(); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); // await vi.advanceTimersToNextTimerAsync(); @@ -81,14 +84,14 @@ describe('Earwurm component', () => { expect(spyState).toBeCalledTimes(1); expect(spyState).toBeCalledWith('running'); - testManager.stop(); + mockManager.stop(); // `suspending/suspended` are called in rapid succession. expect(spyState).toBeCalledTimes(3); expect(spyState).toBeCalledWith('suspending'); expect(spyState).toBeCalledWith('suspended'); - testManager.teardown(); + mockManager.teardown(); expect(spyState).toBeCalledTimes(4); expect(spyState).toBeCalledWith('closed'); @@ -101,35 +104,32 @@ describe('Earwurm component', () => { describe('playing', () => { it('is `true` when any Sound is `playing`', async () => { - const testManager = new Earwurm(); const mockEntry = mockEntries[0]; - testManager.add(mockEntry); + mockManager.add(mockEntry); - const stack = testManager.get(mockEntry.id); + const stack = mockManager.get(mockEntry.id); const sound = await stack?.prepare(); - expect(testManager.playing).toBe(false); + expect(mockManager.playing).toBe(false); sound?.play(); - expect(testManager.playing).toBe(true); + expect(mockManager.playing).toBe(true); vi.advanceTimersByTime(mockData.playDurationMs); - expect(testManager.playing).toBe(false); + expect(mockManager.playing).toBe(false); }); }); describe('get()', () => { - const testManager = new Earwurm(); - it('returns `undefined` when there is no matching Stack', async () => { - const requestedStack = testManager.get('FakeId'); + const requestedStack = mockManager.get('FakeId'); expect(requestedStack).toBe(undefined); }); it('returns the requested Stack when present', async () => { const mockEntry = mockEntries[0]; - testManager.add(mockEntry); + mockManager.add(mockEntry); - const requestedStack = testManager.get(mockEntry.id); + const requestedStack = mockManager.get(mockEntry.id); expect(requestedStack).toBeInstanceOf(Stack); expect(requestedStack?.id).toBe(mockEntry.id); @@ -137,18 +137,16 @@ describe('Earwurm component', () => { }); describe('has()', () => { - const testManager = new Earwurm(); - it('returns `false` when there is no matching Stack', async () => { - const hasStack = testManager.has('FakeId'); + const hasStack = mockManager.has('FakeId'); expect(hasStack).toBe(false); }); it('returns `true` when the requested Stack is present', async () => { const mockEntry = mockEntries[0]; - testManager.add(mockEntry); + mockManager.add(mockEntry); - const hasStack = testManager.has(mockEntry.id); + const hasStack = mockManager.has(mockEntry.id); expect(hasStack).toBe(true); }); }); @@ -157,60 +155,51 @@ describe('Earwurm component', () => { const clickEvent = new Event('click'); it('unlocks AudioContext if not already unlocked', async () => { - const testManager = new Earwurm(); + expect(mockManager.unlocked).toBe(false); - expect(testManager.unlocked).toBe(false); - - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(testManager.unlocked).toBe(true); + expect(mockManager.unlocked).toBe(true); }); it('does not unlock if already unlocked', async () => { - const testManager = new Earwurm(); - const spyStateChange: ManagerEventMap['statechange'] = vi.fn( - (_state) => {}, - ); + const spyState: ManagerEventMap['state'] = vi.fn((_current) => {}); - testManager.on('statechange', spyStateChange); - expect(spyStateChange).not.toBeCalled(); + mockManager.on('state', spyState); + expect(spyState).not.toBeCalled(); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(spyStateChange).toBeCalledTimes(1); - expect(spyStateChange).toBeCalledWith('running'); + expect(spyState).toBeCalledTimes(1); + expect(spyState).toBeCalledWith('running'); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(spyStateChange).not.toBeCalledTimes(2); + expect(spyState).not.toBeCalledTimes(2); }); it('restores lock upon close', async () => { - const testManager = new Earwurm(); - - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(testManager.unlocked).toBe(true); + expect(mockManager.unlocked).toBe(true); - testManager.teardown(); + mockManager.teardown(); document.dispatchEvent(clickEvent); - expect(testManager.unlocked).toBe(false); + expect(mockManager.unlocked).toBe(false); }); it('returns instance', async () => { - const testManager = new Earwurm(); - - const beforeUnlock = testManager.unlock(); + const beforeUnlock = mockManager.unlock(); document.dispatchEvent(clickEvent); expect(beforeUnlock).toBeInstanceOf(Earwurm); - const afterUnlock = testManager.unlock(); + const afterUnlock = mockManager.unlock(); document.dispatchEvent(clickEvent); expect(afterUnlock).toBeInstanceOf(Earwurm); @@ -219,16 +208,13 @@ describe('Earwurm component', () => { describe('add()', () => { it('creates a new Stack for each entry', async () => { - const testManager = new Earwurm(); - const capturedKeys = testManager.add(...mockEntries); + const capturedKeys = mockManager.add(...mockEntries); - expect(testManager.keys).toHaveLength(mockEntries.length); + expect(mockManager.keys).toHaveLength(mockEntries.length); expect(capturedKeys).toStrictEqual(mockEntries.map(({id}) => id)); }); it('replaces any existing Stacks', async () => { - const testManager = new Earwurm(); - const mockChangedEntries: LibraryEntry[] = [ { id: 'Unique', @@ -244,23 +230,67 @@ describe('Earwurm component', () => { }, ]; - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const stack1 = testManager.get(mockEntries[1].id); - const stack2 = testManager.get(mockEntries[2].id); + const stack1 = mockManager.get(mockEntries[1].id); + const stack2 = mockManager.get(mockEntries[2].id); expect(stack1?.path).toBe(mockEntries[1].path); expect(stack2?.path).toBe(mockEntries[2].path); - expect(testManager.keys).toHaveLength(3); + expect(mockManager.keys).toHaveLength(3); - testManager.add(...mockChangedEntries); + mockManager.add(...mockChangedEntries); - const updatedStack1 = testManager.get(mockEntries[1].id); - const updatedStack2 = testManager.get(mockEntries[2].id); + const updatedStack1 = mockManager.get(mockEntries[1].id); + const updatedStack2 = mockManager.get(mockEntries[2].id); expect(updatedStack1?.path).toBe(mockChangedEntries[1].path); expect(updatedStack2?.path).toBe(mockChangedEntries[2].path); - expect(testManager.keys).toHaveLength(4); + expect(mockManager.keys).toHaveLength(4); + }); + + it('emits `library` event with new and old `keys`', async () => { + const spyLibrary: ManagerEventMap['library'] = vi.fn((_new, _old) => {}); + + mockManager.on('library', spyLibrary); + expect(spyLibrary).not.toBeCalled(); + + mockManager.add(...mockEntries); + expect(spyLibrary).toBeCalledWith(mockInitialKeys, []); + expect(spyLibrary).toBeCalledTimes(1); + + // Does not add/remove when both `id + path` are identical. + mockManager.add(mockEntries[0]); + expect(spyLibrary).not.toBeCalledTimes(2); + + const mockUniqueEntry: LibraryEntry = { + id: 'Unique', + path: 'does/not/overwrite/anything.wav', + }; + const mockChangedEntries: LibraryEntry[] = [ + mockUniqueEntry, + mockEntries[1], + ]; + + mockManager.add(...mockChangedEntries); + expect(spyLibrary).toBeCalledTimes(2); + expect(spyLibrary).toBeCalledWith( + [...mockInitialKeys, mockUniqueEntry.id], + mockInitialKeys, + ); + + const keysSnapshot = mockManager.keys; + + // Emits twice as an existing key is removed then re-added + // as a result of the `path` value changing. + mockManager.add({...mockUniqueEntry, path: 'changed'}); + expect(spyLibrary).toBeCalledTimes(4); + + expect(spyLibrary).toBeCalledWith(mockInitialKeys, keysSnapshot); + expect(spyLibrary).toHaveBeenLastCalledWith( + keysSnapshot, + mockInitialKeys, + ); }); // TODO: Figure out how best to read `fadeMs` and `request` from Stack. @@ -275,42 +305,49 @@ describe('Earwurm component', () => { }, }; - const testManager = new Earwurm(mockConfig); - testManager.add(...mockEntries); + const configManager = new Earwurm(mockConfig); + configManager.add(...mockEntries); - const stack = testManager.get(mockEntries[0].id); + const stack = configManager.get(mockEntries[0].id); expect(stack).toBeInstanceOf(Stack); }); }); describe('remove()', () => { it('removes any present Stacks', async () => { - const testManager = new Earwurm(); + mockManager.add(...mockEntries); - testManager.add(...mockEntries); + const mockRemovedKeys: StackId[] = [mockEntries[0].id, mockEntries[1].id]; + const capturedKeys = mockManager.remove(...mockRemovedKeys); - const mockRemovedKeys: LibraryKeys = [ - mockEntries[0].id, - mockEntries[1].id, - ]; - const capturedKeys = testManager.remove(...mockRemovedKeys); - - expect(testManager.keys).toStrictEqual([mockEntries[2].id]); + expect(mockManager.keys).toStrictEqual([mockEntries[2].id]); expect(capturedKeys).toStrictEqual(mockRemovedKeys); }); it('returns empty array when no Stacks match', async () => { - const testManager = new Earwurm(); - - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const capturedKeys = testManager.remove('Foo', 'Bar'); + const capturedKeys = mockManager.remove('Foo', 'Bar'); expect(capturedKeys).toStrictEqual([]); }); - it('tears down Stacks before removing from library', async () => { - const testManager = new Earwurm(); + it('emits `library` event with new and old `keys`', async () => { + const spyLibrary: ManagerEventMap['library'] = vi.fn((_new, _old) => {}); + mockManager.add(...mockEntries); + mockManager.on('library', spyLibrary); + + mockManager.remove('Foo', 'Bar'); + expect(spyLibrary).not.toBeCalled(); + + mockManager.remove(mockEntries[1].id); + expect(spyLibrary).toBeCalledWith( + [mockEntries[0].id, mockEntries[2].id], + mockInitialKeys, + ); + }); + + it('tears down Stacks before removing from library', async () => { const mockChangedEntries: LibraryEntry[] = [ { id: mockEntries[1].id, @@ -322,49 +359,47 @@ describe('Earwurm component', () => { }, ]; - const spyStack1StateChange = vi.fn(); - const spyStack2StateChange = vi.fn(); + const spyStack1State = vi.fn(); + const spyStack2State = vi.fn(); - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const stack1 = testManager.get(mockEntries[1].id); - stack1?.on('statechange', spyStack1StateChange); + const stack1 = mockManager.get(mockEntries[1].id); + stack1?.on('state', spyStack1State); await stack1?.prepare().then((sound) => sound.play()); - expect(spyStack1StateChange).toBeCalledTimes(3); + expect(spyStack1State).toBeCalledTimes(3); expect(stack1?.state).toBe('playing'); - const stack2 = testManager.get(mockEntries[2].id); - stack2?.on('statechange', spyStack2StateChange); + const stack2 = mockManager.get(mockEntries[2].id); + stack2?.on('state', spyStack2State); await stack2?.prepare().then((sound) => sound.play()); - expect(spyStack2StateChange).toBeCalledTimes(3); + expect(spyStack2State).toBeCalledTimes(3); expect(stack2?.state).toBe('playing'); - testManager.add(...mockChangedEntries); + mockManager.add(...mockChangedEntries); - expect(spyStack1StateChange).toBeCalledTimes(4); + expect(spyStack1State).toBeCalledTimes(4); expect(stack1?.state).toBe('idle'); - expect(spyStack2StateChange).toBeCalledTimes(4); + expect(spyStack2State).toBeCalledTimes(4); expect(stack2?.state).toBe('idle'); }); }); describe('stop()', () => { it('stops all playing sounds with each Stack', async () => { - const testManager = new Earwurm(); - const stacks: Stack[] = []; const stackCount = mockEntries.length; const sounds: Array> = []; const soundsPerStack = 4; - testManager.add(...mockEntries); + mockManager.add(...mockEntries); for (let i = 0; i < stackCount; i++) { - const matchedStack = testManager.get(mockEntries[i].id); + const matchedStack = mockManager.get(mockEntries[i].id); if (matchedStack) stacks.push(matchedStack); } @@ -378,21 +413,19 @@ describe('Earwurm component', () => { sound.play(); } - expect(testManager.playing).toBe(true); - expect(testManager.keys).toHaveLength(3); + expect(mockManager.playing).toBe(true); + expect(mockManager.keys).toHaveLength(3); - testManager.stop(); + mockManager.stop(); - expect(testManager.playing).toBe(false); - expect(testManager.keys).toHaveLength(3); + expect(mockManager.playing).toBe(false); + expect(mockManager.keys).toHaveLength(3); }); it('returns instance', async () => { - const testManager = new Earwurm(); + mockManager.add(...mockEntries); - testManager.add(...mockEntries); - - const result = testManager.stop(); + const result = mockManager.stop(); expect(result).toBeInstanceOf(Earwurm); }); }); @@ -401,124 +434,125 @@ describe('Earwurm component', () => { const clickEvent = new Event('click'); it('calls `teardown` on every Stack', async () => { - const testManager = new Earwurm(); const spyStackTeardown = vi.spyOn(Stack.prototype, 'teardown'); - testManager.add(...mockEntries); - testManager.teardown(); + mockManager.add(...mockEntries); + mockManager.teardown(); expect(spyStackTeardown).toBeCalledTimes(mockEntries.length); }); it('empties the `library`', async () => { - const testManager = new Earwurm(); + mockManager.add(...mockEntries); + expect(mockManager.keys).toHaveLength(mockEntries.length); + + mockManager.teardown(); + expect(mockManager.keys).toStrictEqual([]); + }); + + it('emits `library` event with empty array', async () => { + const spyLibrary: ManagerEventMap['library'] = vi.fn((_new, _old) => {}); - testManager.add(...mockEntries); - expect(testManager.keys).toHaveLength(mockEntries.length); + mockManager.add(...mockEntries); - testManager.teardown(); - expect(testManager.keys).toStrictEqual([]); + mockManager.on('library', spyLibrary); + expect(spyLibrary).not.toBeCalled(); + + mockManager.teardown(); + expect(spyLibrary).toBeCalledWith([], mockInitialKeys); }); it('does not resume the AudioContext', async () => { - const testManager = new Earwurm(); - - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); vi.advanceTimersByTime(tokens.suspendAfterMs - 1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); - testManager.teardown(); - expect(testManager.state).toBe('closed'); + mockManager.teardown(); + expect(mockManager.state).toBe('closed'); // You shouldn't really be able to add entries once closed... - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const stack0 = testManager.get(mockEntries[0].id); + const stack0 = mockManager.get(mockEntries[0].id); const stack0Sound = await stack0?.prepare(); - expect(testManager.state).toBe('closed'); + expect(mockManager.state).toBe('closed'); stack0Sound?.play(); - expect(testManager.state).toBe('closed'); + expect(mockManager.state).toBe('closed'); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(testManager.state).toBe('closed'); + expect(mockManager.state).toBe('closed'); }); it('closes the AudioContext', async () => { - const testManager = new Earwurm(); const spyClose = vi.spyOn(AudioContext.prototype, 'close'); - testManager.teardown(); + mockManager.teardown(); expect(spyClose).toBeCalled(); }); it('removes state change listener, preventing further state changes', async () => { - const testManager = new Earwurm(); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); - testManager.teardown(); - expect(testManager.state).toBe('closed'); + mockManager.teardown(); + expect(mockManager.state).toBe('closed'); - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const stack = testManager.get(mockEntries[0].id); + const stack = mockManager.get(mockEntries[0].id); const sound = await stack?.prepare(); sound?.play(); // Not sure how to check active events on the `AudioContext`. - expect(testManager.state).toBe('closed'); + expect(mockManager.state).toBe('closed'); }); // TODO: Figure out how to test the emitted error. it('throws error if AudioContext cannot be closed', async () => { - const testManager = new Earwurm(); const mockErrorMessage = 'Mock error message'; - const spyError: ManagerEventMap['error'] = vi.fn((_error) => {}); - testManager.on('error', spyError); + const spyError: ManagerEventMap['error'] = vi.fn((_messages) => {}); + mockManager.on('error', spyError); vi.spyOn(AudioContext.prototype, 'close').mockImplementationOnce(() => { throw new Error(mockErrorMessage); }); - expect(() => testManager.teardown()).toThrowError(mockErrorMessage); + expect(() => mockManager.teardown()).toThrowError(mockErrorMessage); /* expect(spyError).toBeCalledWith([ - 'Failed to close the Earwurm AudioContext.', + Earwurm.errorMessage.close, mockErrorMessage, ]); */ }); it('removes any event listeners', async () => { - const testManager = new Earwurm(); + const spyState = vi.fn(); const spyError = vi.fn(); - const spyStateChange = vi.fn(); - testManager.on('error', spyError); - testManager.on('statechange', spyStateChange); + mockManager.on('state', spyState); + mockManager.on('error', spyError); - expect(testManager.activeEvents).toHaveLength(2); - testManager.teardown(); - expect(testManager.activeEvents).toHaveLength(0); + expect(mockManager.activeEvents).toHaveLength(2); + mockManager.teardown(); + expect(mockManager.activeEvents).toHaveLength(0); }); it('returns instance', async () => { - const testManager = new Earwurm(); - - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const result = testManager.teardown(); + const result = mockManager.teardown(); expect(result).toBeInstanceOf(Earwurm); }); }); @@ -527,18 +561,17 @@ describe('Earwurm component', () => { const clickEvent = new Event('click'); it('registers once `state` is `running`', async () => { - const testManager = new Earwurm(); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(tokens.suspendAfterMs - 1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(1); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); }); // TODO: Figure out a way to force `AudioContext.state` @@ -546,27 +579,26 @@ describe('Earwurm component', () => { it.todo('can register upon initialization'); it('resets countdown when Stack states change', async () => { - const testManager = new Earwurm(); - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - const stack0 = testManager.get(mockEntries[0].id); + const stack0 = mockManager.get(mockEntries[0].id); // Beginning of suspension timer. - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(tokens.suspendAfterMs - 1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); // `Sound` preparation triggers `Stack` state changes, // resulting in a suspension timer reset. const stack0Sound = await stack0?.prepare(); vi.advanceTimersByTime(1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(tokens.suspendAfterMs - 2); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); expect(stack0?.playing).toBe(false); // Another suspension timer reset. @@ -574,36 +606,33 @@ describe('Earwurm component', () => { expect(stack0?.playing).toBe(true); vi.advanceTimersByTime(1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); // End of `Sound` + Reset of suspension timer. vi.advanceTimersByTime(mockData.playDurationMs - 1); expect(stack0?.playing).toBe(false); vi.advanceTimersByTime(tokens.suspendAfterMs - 1); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(1); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); }); it('triggers state changes', async () => { - const testManager = new Earwurm(); - const spyStateChange: ManagerEventMap['statechange'] = vi.fn( - (_state) => {}, - ); + const spyState: ManagerEventMap['state'] = vi.fn((_current) => {}); - testManager.on('statechange', spyStateChange); + mockManager.on('state', spyState); - expect(spyStateChange).not.toBeCalled(); + expect(spyState).not.toBeCalled(); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - expect(spyStateChange).toBeCalledWith('running'); + expect(spyState).toBeCalledWith('running'); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(spyStateChange).toBeCalledWith('suspending'); - expect(spyStateChange).toBeCalledWith('suspended'); + expect(spyState).toBeCalledWith('suspending'); + expect(spyState).toBeCalledWith('suspended'); }); // TODO: Is there any good way / value in testing these conditions? @@ -611,19 +640,17 @@ describe('Earwurm component', () => { it.todo('does not re-register if already `suspended`'); it('does not allow suspension if `closed`', async () => { - const testManager = new Earwurm(); - - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); - testManager.teardown(); - expect(testManager.state).toBe('closed'); + mockManager.teardown(); + expect(mockManager.state).toBe('closed'); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(testManager.state).toBe('closed'); + expect(mockManager.state).toBe('closed'); }); }); @@ -631,27 +658,26 @@ describe('Earwurm component', () => { const clickEvent = new Event('click'); it('resumes once a `Sound` plays', async () => { - const testManager = new Earwurm(); - testManager.add(...mockEntries); + mockManager.add(...mockEntries); - testManager.unlock(); + mockManager.unlock(); document.dispatchEvent(clickEvent); - const stack0 = testManager.get(mockEntries[0].id); + const stack0 = mockManager.get(mockEntries[0].id); const stack0Sound = await stack0?.prepare(); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); stack0Sound?.play(); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(mockData.playDurationMs); - expect(testManager.state).toBe('running'); + expect(mockManager.state).toBe('running'); vi.advanceTimersByTime(tokens.suspendAfterMs); - expect(testManager.state).toBe('suspended'); + expect(mockManager.state).toBe('suspended'); }); // TODO: Cannot test this until we can "interrupt" the suspension. @@ -661,6 +687,7 @@ describe('Earwurm component', () => { it('throws error if the resume fails'); }); - // Both `statechange` and `error` are covered in other tests. + // All events are covered in other tests: + // `state`, `library`, `volume`, `mute`, and `error`. // describe('events', () => {}); }); diff --git a/src/tests/Sound.test.ts b/src/tests/Sound.test.ts index 0bebf4a..76c8537 100644 --- a/src/tests/Sound.test.ts +++ b/src/tests/Sound.test.ts @@ -1,7 +1,8 @@ -import {describe, it, expect, vi} from 'vitest'; +import {afterEach, describe, it, expect, vi} from 'vitest'; import {Sound} from '../Sound'; -import type {SoundEventMap} from '../types'; +import {tokens} from '../tokens'; +import type {SoundState, SoundEventMap} from '../types'; type SoundConstructor = ConstructorParameters; @@ -23,13 +24,20 @@ describe('Sound component', () => { defaultAudioNode, ); - it('is initialized with default values', () => { + it('is initialized with default values', async () => { expect(testSound).toBeInstanceOf(Sound); expect(testSound).toHaveProperty('volume', 1); expect(testSound).toHaveProperty('mute', false); + expect(testSound).toHaveProperty('speed', 1); expect(testSound).toHaveProperty('loop', false); expect(testSound).toHaveProperty('duration', 0); + expect(testSound).toHaveProperty('progress', { + elapsed: 0, + remaining: 0, + percentage: 0, + iterations: 0, + }); expect(testSound).toHaveProperty('state', 'created'); }); }); @@ -40,20 +48,175 @@ describe('Sound component', () => { // `volume` accessor is covered in `Abstract.test.ts`. // describe('volume', () => {}); - describe('loop', () => { + describe('speed', () => { const testSound = new Sound( - 'TestLoop', + 'TestSpeed', defaultAudioBuffer, defaultContext, defaultAudioNode, ); - it('allows `set` and `get`', () => { - // TODO: Should check `AudioBufferSourceNode` value. + afterEach(() => { + testSound.speed = 1; + }); + + it('allows `set` and `get`', async () => { + const mockSpeed = 0.4; + testSound.speed = mockSpeed; + + expect(testSound.speed).toBe(mockSpeed); + }); + + it('restricts value to a minimum of `0.25`', async () => { + testSound.speed = -10; + expect(testSound.speed).toBe(tokens.minSpeed); + }); + + it('restricts value to a maximum of `4`', async () => { + testSound.speed = 10; + expect(testSound.speed).toBe(tokens.maxSpeed); + }); + + // TODO: Not using `linearRamp` at the moment because: + // 1. We are not usign "transitions" yet on `speed` change. + // 2. FireFox seems to have a problem pausing after a + // `speed` change when using `linearRamp`. Likely due to + // either the same `to/from > currentTime` or no call to + // `cancelScheduledValues()` before setting `tokens.pauseSpeed`. + it.skip('sets value on `playbackRate`', async () => { + const oldValue = testSound.speed; + const newValue = 2.2; + + const spySourceCancel = vi.spyOn( + AudioParam.prototype, + 'cancelScheduledValues', + ); + const spySourceSet = vi.spyOn(AudioParam.prototype, 'setValueAtTime'); + const spySourceRamp = vi.spyOn( + AudioParam.prototype, + 'linearRampToValueAtTime', + ); + + const {currentTime} = defaultContext; + testSound.play(); + + // TODO: Spy on the `playbackRate.value` setter. + // const spyPlaybackRateSet = vi.spyOn(AudioParam.prototype, 'value', 'set'); + testSound.speed = newValue; + + expect(spySourceCancel).toBeCalledWith(currentTime); + expect(spySourceSet).toBeCalledWith(oldValue, currentTime); + expect(spySourceRamp).toBeCalledWith(newValue, currentTime); + }); + + // TODO: Skipping for same reasons as above. + it.skip('does not set value on playbackRate if paused', async () => { + const {currentTime} = defaultContext; + // TODO: This test should be changed to directly check that + // the `AudioParam` has the expected `tokens` value. + const spyRamp = vi.spyOn(AudioParam.prototype, 'linearRampToValueAtTime'); + + testSound.play(); + expect(spyRamp).not.toBeCalled(); + + testSound.speed = 1.23; + expect(spyRamp).toBeCalledTimes(1); + expect(spyRamp).toBeCalledWith(1.23, currentTime); + + testSound.pause(); + testSound.speed = 2.34; + expect(spyRamp).not.toBeCalledTimes(2); + + testSound.play(); + testSound.speed = 3.45; + expect(spyRamp).toBeCalledTimes(2); + expect(spyRamp).toBeCalledWith(3.45, currentTime); + }); + + // TODO: Need to figure out how to spy on `playbackRate.value`. + it.todo('sets value directly on AudioParam when not `paused` or `playing`'); + + // TODO: Author this test if/when we support "transitions". + it.todo('transitions to new speed'); + + it('triggers speed event when set to a unique value', async () => { + const spySpeed: SoundEventMap['speed'] = vi.fn((_rate) => {}); + + testSound.on('speed', spySpeed); + expect(spySpeed).not.toBeCalled(); + + testSound.speed = 1; + expect(spySpeed).not.toBeCalled(); + + testSound.speed = 1.23; + expect(spySpeed).toBeCalledWith(1.23); + + testSound.speed = 1.23; + expect(spySpeed).not.toBeCalledTimes(2); + + testSound.off('speed', spySpeed); + + testSound.speed = 2.34; + expect(spySpeed).not.toBeCalledWith(2.34); + }); + }); + + describe('loop', () => { + const mockConstructorArgs: SoundConstructor = [ + 'TestLoop', + defaultAudioBuffer, + defaultContext, + defaultAudioNode, + ]; + + it('allows `set` and `get`', async () => { + const testSound = new Sound(...mockConstructorArgs); + + // TODO: Should check that `loop` is set on the source. + // const spySourceNode = vi.spyOn(AudioBufferSourceNode.prototype, 'loop', 'set'); + expect(testSound.loop).toBe(false); testSound.loop = true; expect(testSound.loop).toBe(true); }); + + it('does not repeat by default', async () => { + const testSound = new Sound(...mockConstructorArgs); + + expect(testSound.progress.iterations).toBe(0); + testSound.play(); + vi.advanceTimersToNextTimer(); + expect(testSound.progress.iterations).toBe(0); + vi.advanceTimersToNextTimer(); + expect(testSound.progress.iterations).toBe(0); + }); + + it('repeats sound indefinitely', async () => { + const testSound = new Sound(...mockConstructorArgs); + + expect(testSound.state).toBe('created'); + expect(testSound.progress.iterations).toBe(0); + + testSound.loop = true; + testSound.play(); + + expect(testSound.state).toBe('playing'); + vi.advanceTimersToNextTimer(); + expect(testSound.progress.iterations).toBe(1); + vi.advanceTimersToNextTimer(); + expect(testSound.progress.iterations).toBe(2); + + // TODO: Test env seems to trigger `ended` even though + // we are looping the Sound. + // expect(testSound.state).toBe('playing'); + + testSound.loop = false; + vi.advanceTimersToNextTimer(); + // `iterations` does not increment a final time, + // as it is only updated at the START of a new iteration. + expect(testSound.progress.iterations).toBe(2); + expect(testSound.state).toBe('ending'); + }); }); describe('duration', () => { @@ -64,7 +227,7 @@ describe('Sound component', () => { defaultAudioNode, ); - it('allows `get`', () => { + it('allows `get`', async () => { // TODO: We should provide a buffer that has a `duration`. expect(testSound.duration).toBe(0); }); @@ -81,7 +244,7 @@ describe('Sound component', () => { defaultAudioNode, ]; - it('starts playing the source', () => { + it('starts playing the source', async () => { const testSound = new Sound(...mockConstructorArgs); const spySourceStart = vi.spyOn(AudioBufferSourceNode.prototype, 'start'); @@ -90,7 +253,7 @@ describe('Sound component', () => { expect(spySourceStart).toBeCalledTimes(1); }); - it('does not call `start()` a 2nd time', () => { + it('does not call `start()` a 2nd time', async () => { const testSound = new Sound(...mockConstructorArgs); const spySourceStart = vi.spyOn(AudioBufferSourceNode.prototype, 'start'); @@ -98,27 +261,28 @@ describe('Sound component', () => { expect(spySourceStart).toBeCalledTimes(1); }); - it('unpauses the source', () => { + // TODO: Figure out how to directly test against + // `AudioBufferSourceNode.playbackRate.value`. + it.skip('unpauses the source', async () => { const testSound = new Sound(...mockConstructorArgs); - // TODO: Figure out how to check `#source` for `playbackRate.value`. - expect(testSound.mute).toBe(false); + expect(testSound.speed).toBe(1); testSound.play(); - expect(testSound.mute).toBe(false); + expect(testSound.speed).toBe(1); testSound.pause(); - expect(testSound.mute).toBe(true); + expect(testSound.speed).toBe(tokens.pauseSpeed); testSound.play(); - expect(testSound.mute).toBe(false); + expect(testSound.speed).toBe(1); }); - it('updates state', () => { + it('updates state', async () => { const testSound = new Sound(...mockConstructorArgs); testSound.play(); expect(testSound.state).toBe('playing'); }); - it('returns instance', () => { + it('returns instance', async () => { const testSound = new Sound(...mockConstructorArgs); const instance = testSound.play(); @@ -134,17 +298,18 @@ describe('Sound component', () => { defaultAudioNode, ]; - it('pauses the source', () => { + // TODO: Figure out how to directly test against + // `AudioBufferSourceNode.playbackRate.value`. + it.skip('pauses the source', async () => { const testSound = new Sound(...mockConstructorArgs); - // TODO: Figure out how to check `#source` for `playbackRate.value`. testSound.play(); - expect(testSound.mute).toBe(false); + expect(testSound.speed).toBe(1); testSound.pause(); - expect(testSound.mute).toBe(true); + expect(testSound.speed).toBe(tokens.pauseSpeed); }); - it('updates state', () => { + it('updates state', async () => { const testSound = new Sound(...mockConstructorArgs); testSound.play().pause(); @@ -154,7 +319,7 @@ describe('Sound component', () => { // This condition is already covered in `events`. // it('does not update state again if already paused'); - it('returns instance', () => { + it('returns instance', async () => { const testSound = new Sound(...mockConstructorArgs); const instance = testSound.play().pause(); @@ -170,7 +335,7 @@ describe('Sound component', () => { defaultAudioNode, ]; - it('stops and disconnects the source', () => { + it('stops and disconnects the source', async () => { const testSound = new Sound(...mockConstructorArgs); const spySourceStop = vi.spyOn(AudioBufferSourceNode.prototype, 'stop'); const spySourceDisconnect = vi.spyOn( @@ -187,14 +352,19 @@ describe('Sound component', () => { expect(spySourceDisconnect).toBeCalledTimes(1); }); - it('updates state', () => { + it('emits the `stopping` state before `ending`', async () => { const testSound = new Sound(...mockConstructorArgs); + const activeStates: SoundState[] = []; + testSound.on('state', (current) => activeStates.push(current)); + + expect(testSound.state).toBe('created'); testSound.play().stop(); - expect(testSound.state).toBe('stopping'); + expect(activeStates).toStrictEqual(['playing', 'stopping', 'ending']); + expect(testSound.state).toBe('ending'); }); - it('empties active events', () => { + it('empties active events', async () => { const testSound = new Sound(...mockConstructorArgs); testSound.on('ended', vi.fn()); @@ -205,7 +375,7 @@ describe('Sound component', () => { expect(testSound.activeEvents).toHaveLength(0); }); - it('returns instance', () => { + it('returns instance', async () => { const testSound = new Sound(...mockConstructorArgs); const instance = testSound.play().stop(); @@ -213,6 +383,8 @@ describe('Sound component', () => { }); }); + // Some events are covered in other tests: + // `volume`, `mute`, and `speed`. describe('events', () => { const mockConstructorArgs: SoundConstructor = [ 'TestEvents', @@ -221,60 +393,138 @@ describe('Sound component', () => { defaultAudioNode, ]; - it('emits an event for every statechange', () => { - const testSound = new Sound(...mockConstructorArgs); - const spyStateChange: SoundEventMap['statechange'] = vi.fn( - (_state) => {}, - ); + describe('state', () => { + it('emits for every change', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyState: SoundEventMap['state'] = vi.fn((_current) => {}); - testSound.on('statechange', spyStateChange); + testSound.on('state', spyState); + expect(spyState).not.toBeCalled(); - expect(spyStateChange).not.toBeCalled(); + testSound.play(); + expect(spyState).toBeCalledWith('playing'); - testSound.play(); - expect(spyStateChange).toBeCalledWith('playing'); + testSound.pause(); + expect(spyState).toBeCalledWith('paused'); - testSound.pause(); - expect(spyStateChange).toBeCalledWith('paused'); + testSound.stop(); + expect(spyState).toBeCalledWith('stopping'); + }); - testSound.stop(); - expect(spyStateChange).toBeCalledWith('stopping'); + it('does not emit redundant changes', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyState: SoundEventMap['state'] = vi.fn((_current) => {}); + + testSound.on('state', spyState); + + testSound.play(); + expect(spyState).toBeCalledTimes(1); + expect(spyState).toBeCalledWith('playing'); + + testSound.pause(); + expect(spyState).toBeCalledTimes(2); + expect(spyState).toBeCalledWith('paused'); + + testSound.pause(); + expect(spyState).not.toBeCalledTimes(3); + }); }); - it('does not emit redundant state changes', () => { - const testSound = new Sound(...mockConstructorArgs); - const spyStateChange: SoundEventMap['statechange'] = vi.fn( - (_state) => {}, - ); + describe('ended', () => { + it('emits once sound has finished', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyEnded: SoundEventMap['ended'] = vi.fn((_event) => {}); - testSound.on('statechange', spyStateChange); + testSound.on('ended', spyEnded); + testSound.play(); - testSound.play(); - expect(spyStateChange).toBeCalledTimes(1); - expect(spyStateChange).toBeCalledWith('playing'); + expect(spyEnded).not.toBeCalled(); + vi.advanceTimersToNextTimer(); - testSound.pause(); - expect(spyStateChange).toBeCalledTimes(2); - expect(spyStateChange).toBeCalledWith('paused'); + expect(spyEnded).toBeCalledWith({ + id: testSound.id, + source: expect.any(AudioBufferSourceNode), + neverStarted: false, + }); + }); - testSound.pause(); - expect(spyStateChange).not.toBeCalledTimes(3); + it('emits for a stopped sound that was never started', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyEnded: SoundEventMap['ended'] = vi.fn((_event) => {}); + + testSound.on('ended', spyEnded); + + expect(spyEnded).not.toBeCalled(); + vi.advanceTimersToNextTimer(); + + testSound.stop(); + + expect(spyEnded).toBeCalledWith({ + id: testSound.id, + source: expect.any(AudioBufferSourceNode), + neverStarted: true, + }); + }); }); - it('emits `ended` event once sound has finished', () => { - const testSound = new Sound(...mockConstructorArgs); - const spyEnded: SoundEventMap['ended'] = vi.fn((_event) => {}); + describe('progress', () => { + it('emits for every animation frame', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyProgressEvent: SoundEventMap['progress'] = vi.fn( + (_event) => {}, + ); + const spyGetProgress = vi.spyOn(Sound.prototype, 'progress', 'get'); + + testSound.on('progress', spyProgressEvent); + testSound.play(); + + expect(spyProgressEvent).not.toBeCalled(); + expect(spyGetProgress).not.toBeCalled(); + vi.advanceTimersToNextTimer(); + + // TODO: We will need to mock this test so that we can actually + // increment `currentTime` against a playing Sound. + expect(spyProgressEvent).toBeCalledTimes(1); + expect(spyGetProgress).toBeCalledTimes(1); + + expect(spyProgressEvent).toBeCalledWith({ + elapsed: 0, + remaining: 0, + percentage: 0, + iterations: 0, + }); + }); - testSound.on('ended', spyEnded); - testSound.play(); + it('does not perform unneccessary calculations when there are no subscriptions', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyGetProgress = vi.spyOn(Sound.prototype, 'progress', 'get'); - expect(spyEnded).not.toBeCalled(); - vi.advanceTimersToNextTimer(); + testSound.play(); + vi.advanceTimersToNextTimer(); + + expect(spyGetProgress).not.toBeCalled(); + }); - expect(spyEnded).toBeCalledWith({ - id: testSound.id, - source: expect.any(AudioBufferSourceNode), + // TODO: This test needs to instead check against + // `requestAnimationFrame()` never getting registered. + // We actually do still `emit` at the end of `setState()`. + it('does not emit events when subscribed after the sound has been started', async () => { + const testSound = new Sound(...mockConstructorArgs); + const spyProgressEvent: SoundEventMap['progress'] = vi.fn( + (_event) => {}, + ); + + testSound.play(); + vi.advanceTimersToNextTimer(); + testSound.on('progress', spyProgressEvent); + + expect(spyProgressEvent).not.toBeCalled(); }); + + // TODO: We need to correctly mock a sound's duration and playback. + // This test should cover a combination of: + // changing speeds during playback, looping, and pausing. + it.todo('calculates progress values at various points in playback'); }); }); }); diff --git a/src/tests/Stack.test.ts b/src/tests/Stack.test.ts index f349308..5309601 100644 --- a/src/tests/Stack.test.ts +++ b/src/tests/Stack.test.ts @@ -1,4 +1,4 @@ -import {describe, it, expect, vi} from 'vitest'; +import {afterEach, describe, it, expect, vi} from 'vitest'; import {Stack} from '../Stack'; import {Sound} from '../Sound'; @@ -12,27 +12,33 @@ type StackConstructor = ConstructorParameters; describe('Stack component', () => { const defaultContext = new AudioContext(); const defaultAudioNode = new AudioNode(); + const defaultConstructorArgs: StackConstructor = [ + 'MockStack', + mockData.audio, + defaultContext, + defaultAudioNode, + ]; + + let mockStack = new Stack(...defaultConstructorArgs); + + afterEach(() => { + mockStack.teardown(); + mockStack = new Stack(...defaultConstructorArgs); + }); describe('initialization', () => { - const testStack = new Stack( - 'TestInit', - mockData.audio, - defaultContext, - defaultAudioNode, - ); - - it('is initialized with default values', () => { - expect(testStack).toBeInstanceOf(Stack); + it('is initialized with default values', async () => { + expect(mockStack).toBeInstanceOf(Stack); // Class static properties expect(Stack).toHaveProperty('maxStackSize', tokens.maxStackSize); // Instance properties - expect(testStack).toHaveProperty('volume', 1); - expect(testStack).toHaveProperty('mute', false); - expect(testStack).toHaveProperty('keys', []); - expect(testStack).toHaveProperty('state', 'idle'); - expect(testStack).toHaveProperty('playing', false); + expect(mockStack).toHaveProperty('volume', 1); + expect(mockStack).toHaveProperty('mute', false); + expect(mockStack).toHaveProperty('keys', []); + expect(mockStack).toHaveProperty('state', 'idle'); + expect(mockStack).toHaveProperty('playing', false); }); }); @@ -43,22 +49,13 @@ describe('Stack component', () => { // describe('volume', () => {}); describe('keys', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestKeys', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('contains ids of each unexpired Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); - const mockDurationHalf = Math.floor(mockData.playDurationMs / 2); const mockDurationQuarter = Math.floor(mockData.playDurationMs / 4); - const sound1 = await testStack.prepare('One'); - const sound2 = await testStack.prepare('Two'); - const sound3 = await testStack.prepare('Three'); + const sound1 = await mockStack.prepare('One'); + const sound2 = await mockStack.prepare('Two'); + const sound3 = await mockStack.prepare('Three'); sound1.play(); vi.advanceTimersByTime(mockDurationQuarter); @@ -66,34 +63,26 @@ describe('Stack component', () => { vi.advanceTimersByTime(mockDurationQuarter); sound3.play(); - expect(testStack.keys).toStrictEqual(['One', 'Two', 'Three']); + expect(mockStack.keys).toStrictEqual(['One', 'Two', 'Three']); vi.advanceTimersByTime(mockDurationHalf); - expect(testStack.keys).toStrictEqual(['Two', 'Three']); + expect(mockStack.keys).toStrictEqual(['Two', 'Three']); vi.advanceTimersByTime(mockDurationQuarter); - expect(testStack.keys).toStrictEqual(['Three']); + expect(mockStack.keys).toStrictEqual(['Three']); vi.advanceTimersByTime(mockDurationQuarter); - expect(testStack.keys).toStrictEqual([]); + expect(mockStack.keys).toStrictEqual([]); }); }); describe('state', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestState', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; + it('triggers `state` event for every possible value', async () => { + const spyState: StackEventMap['state'] = vi.fn((_current) => {}); - it('triggers `statechange` event for every state', async () => { - const testStack = new Stack(...mockConstructorArgs); - const spyState: StackEventMap['statechange'] = vi.fn((_state) => {}); - - testStack.on('statechange', spyState); + mockStack.on('state', spyState); expect(spyState).not.toBeCalled(); - expect(testStack.state).toBe('idle'); + expect(mockStack.state).toBe('idle'); - const soundFoo = testStack.prepare('Foo'); + const soundFoo = mockStack.prepare('Foo'); expect(spyState).toBeCalledWith('loading'); await soundFoo.then((sound) => sound.play()); @@ -108,54 +97,36 @@ describe('Stack component', () => { }); describe('playing', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestPlaying', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('is `true` when any Sound is `playing`', async () => { - const testStack = new Stack(...mockConstructorArgs); - const sound = await testStack.prepare(); + const sound = await mockStack.prepare(); - expect(testStack.state).toBe('idle'); - expect(testStack.playing).toBe(false); + expect(mockStack.state).toBe('idle'); + expect(mockStack.playing).toBe(false); sound.play(); - expect(testStack.state).toBe('playing'); - expect(testStack.playing).toBe(true); + expect(mockStack.state).toBe('playing'); + expect(mockStack.playing).toBe(true); vi.advanceTimersByTime(mockData.playDurationMs); - expect(testStack.state).toBe('idle'); - expect(testStack.playing).toBe(false); + expect(mockStack.state).toBe('idle'); + expect(mockStack.playing).toBe(false); }); }); describe('get()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestGet', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('returns `undefined` when there is no matching Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); - await testStack.prepare('RealId'); + await mockStack.prepare('RealId'); - const requestedSound = testStack.get('FakeId'); + const requestedSound = mockStack.get('FakeId'); expect(requestedSound).toBe(undefined); }); it('returns the requested Sound when present', async () => { - const testStack = new Stack(...mockConstructorArgs); - const mockSoundId = 'RealId'; - const capturedSound = await testStack.prepare(mockSoundId); - const requestedSound = testStack.get(mockSoundId); + const capturedSound = await mockStack.prepare(mockSoundId); + const requestedSound = mockStack.get(mockSoundId); expect(requestedSound).toBeInstanceOf(Sound); expect(requestedSound).toBe(capturedSound); @@ -163,60 +134,41 @@ describe('Stack component', () => { }); describe('has()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestHas', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('returns `false` when there is no matching Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); - const hasSound = testStack.has('FakeId'); + const hasSound = mockStack.has('FakeId'); expect(hasSound).toBe(false); }); it('returns `true` when the requested Sound is present', async () => { - const testStack = new Stack(...mockConstructorArgs); - const mockSoundId = 'RealId'; - await testStack.prepare(mockSoundId); - const hasSound = testStack.has(mockSoundId); + await mockStack.prepare(mockSoundId); + const hasSound = mockStack.has(mockSoundId); expect(hasSound).toBe(true); }); }); describe('pause()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestPause', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('pauses and retains every Sound within the Stack', async () => { - const testStack = new Stack(...mockConstructorArgs); - - const sound1 = await testStack.prepare('One'); - const sound2 = await testStack.prepare('Two'); - const sound3 = await testStack.prepare('Three'); + const sound1 = await mockStack.prepare('One'); + const sound2 = await mockStack.prepare('Two'); + const sound3 = await mockStack.prepare('Three'); const spySound1Pause = vi.spyOn(sound1, 'pause'); const spySound2Pause = vi.spyOn(sound2, 'pause'); const spySound3Pause = vi.spyOn(sound3, 'pause'); - expect(testStack.state).toBe('idle'); - expect(testStack.keys).toHaveLength(3); + expect(mockStack.state).toBe('idle'); + expect(mockStack.keys).toHaveLength(3); sound1.play(); sound2.play(); sound3.play(); - expect(testStack.state).toBe('playing'); - testStack.pause(); - expect(testStack.state).toBe('idle'); - expect(testStack.keys).toHaveLength(3); + expect(mockStack.state).toBe('playing'); + mockStack.pause(); + expect(mockStack.state).toBe('idle'); + expect(mockStack.keys).toHaveLength(3); expect(spySound1Pause).toBeCalled(); expect(spySound2Pause).toBeCalled(); @@ -224,93 +176,93 @@ describe('Stack component', () => { }); it('returns instance', async () => { - const testStack = new Stack(...mockConstructorArgs); - await testStack.prepare('Foo'); - const instance = testStack.pause(); + await mockStack.prepare('Foo'); + const instance = mockStack.pause(); - expect(instance).toBe(testStack); + expect(instance).toBe(mockStack); }); }); describe('stop()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestStop', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('stops and removes every Sound within the Stack', async () => { - const testStack = new Stack(...mockConstructorArgs); - - const sound1 = await testStack.prepare('One'); - const sound2 = await testStack.prepare('Two'); - const sound3 = await testStack.prepare('Three'); + const sound1 = await mockStack.prepare('One'); + const sound2 = await mockStack.prepare('Two'); + const sound3 = await mockStack.prepare('Three'); const spySound1Stop = vi.spyOn(sound1, 'stop'); const spySound2Stop = vi.spyOn(sound2, 'stop'); const spySound3Stop = vi.spyOn(sound3, 'stop'); - expect(testStack.state).toBe('idle'); - expect(testStack.keys).toHaveLength(3); + expect(mockStack.state).toBe('idle'); + expect(mockStack.keys).toHaveLength(3); sound1.play(); sound2.play(); sound3.play(); - expect(testStack.state).toBe('playing'); - testStack.stop(); - expect(testStack.state).toBe('idle'); - expect(testStack.keys).toHaveLength(0); + expect(mockStack.state).toBe('playing'); + mockStack.stop(); + expect(mockStack.state).toBe('idle'); + expect(mockStack.keys).toHaveLength(0); expect(spySound1Stop).toBeCalled(); expect(spySound2Stop).toBeCalled(); expect(spySound3Stop).toBeCalled(); }); + it('emits `queue` event for each stopped Sound', async () => { + const spyQueue: StackEventMap['queue'] = vi.fn((_new, _old) => {}); + + mockStack.on('queue', spyQueue); + expect(spyQueue).not.toBeCalled(); + + await mockStack.prepare('One'); + await mockStack.prepare('Two'); + await mockStack.prepare('Three'); + + expect(spyQueue).toBeCalledTimes(3); + expect(spyQueue).toHaveBeenLastCalledWith( + ['One', 'Two', 'Three'], + ['One', 'Two'], + ); + + mockStack.stop(); + + expect(spyQueue).toBeCalledTimes(6); + expect(spyQueue).toHaveBeenLastCalledWith([], ['Three']); + }); + it('returns instance', async () => { - const testStack = new Stack(...mockConstructorArgs); - await testStack.prepare('Foo'); - const instance = testStack.stop(); + await mockStack.prepare('Foo'); + const instance = mockStack.stop(); - expect(instance).toBe(testStack); + expect(instance).toBe(mockStack); }); }); describe('teardown()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestTeardown', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('calls stop()', async () => { - const testStack = new Stack(...mockConstructorArgs); - const spyStop = vi.spyOn(testStack, 'stop'); + const spyStop = vi.spyOn(mockStack, 'stop'); expect(spyStop).not.toBeCalled(); - testStack.teardown(); + mockStack.teardown(); expect(spyStop).toBeCalledTimes(1); }); it('empties all active events', async () => { - const testStack = new Stack(...mockConstructorArgs); - - testStack.on('error', vi.fn()); - testStack.on('statechange', vi.fn()); + mockStack.on('state', vi.fn()); + mockStack.on('error', vi.fn()); - expect(testStack.activeEvents).toHaveLength(2); - testStack.teardown(); - expect(testStack.activeEvents).toHaveLength(0); + expect(mockStack.activeEvents).toHaveLength(2); + mockStack.teardown(); + expect(mockStack.activeEvents).toHaveLength(0); }); it('returns instance', async () => { - const testStack = new Stack(...mockConstructorArgs); - await testStack.prepare('Foo'); - const instance = testStack.stop(); + await mockStack.prepare('Foo'); + const instance = mockStack.stop(); - expect(instance).toBe(testStack); + expect(instance).toBe(mockStack); }); }); @@ -356,45 +308,59 @@ describe('Stack component', () => { }); it('returns a Promise containing the newly created Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); const mockSoundId = 'Foo'; - const sound = testStack.prepare(mockSoundId); + const sound = mockStack.prepare(mockSoundId); expect(sound).toBeInstanceOf(Promise); await expect(sound).resolves.toBeInstanceOf(Sound); await expect(sound).resolves.toHaveProperty('id', mockSoundId); }); + + it('emits `queue` event with new and old `keys`', async () => { + const mockSoundId1 = 'Foo'; + const mockSoundId2 = 'Bar'; + const spyQueue: StackEventMap['queue'] = vi.fn((_new, _old) => {}); + + mockStack.on('queue', spyQueue); + expect(spyQueue).not.toBeCalled(); + expect(mockStack.keys).toHaveLength(0); + + await mockStack.prepare(mockSoundId1); + + expect(spyQueue).toBeCalledTimes(1); + expect(spyQueue).toBeCalledWith([mockSoundId1], []); + expect(mockStack.keys).toHaveLength(1); + + await mockStack.prepare(mockSoundId2); + + expect(spyQueue).toBeCalledTimes(2); + expect(spyQueue).toBeCalledWith( + [mockSoundId1, mockSoundId2], + [mockSoundId1], + ); + expect(mockStack.keys).toHaveLength(2); + }); }); describe('#load()', () => { - const mockConstructorArgs: StackConstructor = [ - 'TestLoad', - mockData.audio, - defaultContext, - defaultAudioNode, - ]; - it('sets state to `loading` until fetch is resolved', async () => { - const testStack = new Stack(...mockConstructorArgs); - const sound = testStack.prepare(); + const sound = mockStack.prepare(); - expect(testStack.state).toBe('loading'); + expect(mockStack.state).toBe('loading'); await sound; - expect(testStack.state).toBe('idle'); + expect(mockStack.state).toBe('idle'); }); it('returns state to `playing` if a Sound was already playing', async () => { - const testStack = new Stack(...mockConstructorArgs); - - expect(testStack.state).toBe('idle'); - await testStack.prepare().then((sound) => sound.play()); - expect(testStack.state).toBe('playing'); + expect(mockStack.state).toBe('idle'); + await mockStack.prepare().then((sound) => sound.play()); + expect(mockStack.state).toBe('playing'); - const unplayedSound = testStack.prepare(); + const unplayedSound = mockStack.prepare(); - expect(testStack.state).toBe('loading'); + expect(mockStack.state).toBe('loading'); await unplayedSound; - expect(testStack.state).toBe('playing'); + expect(mockStack.state).toBe('playing'); }); it.todo('passes `request` to `fetchAudioBuffer`'); @@ -410,7 +376,7 @@ describe('Stack component', () => { defaultAudioNode, ); - const spyError: StackEventMap['error'] = vi.fn((_error) => {}); + const spyError: StackEventMap['error'] = vi.fn((_message) => {}); testStack.on('error', spyError); await testStack.prepare(); @@ -419,7 +385,8 @@ describe('Stack component', () => { id: mockStackId, message: [ `Failed to load: ${mockPath}`, - expect.stringContaining(mockPath), + // This string ends with `[object Request]`. + expect.stringContaining('Failed to parse URL from'), ], }); }); @@ -468,15 +435,14 @@ describe('Stack component', () => { expect(sound).toHaveProperty('buffer', { duration: 0, length: 1, - // TODO: This test might fail locally... // If it does, it is because the fetch request needs // to be mocked so that we do not return a scratch buffer. - // numberOfChannels: 1, - // sampleRate: 22050, - - numberOfChannels: 2, - sampleRate: 44100, + // Returned object contains either: + // {numberOfChannels: 1, sampleRate: 22050} + // {numberOfChannels: 2, sampleRate: 44100} + numberOfChannels: 1, + sampleRate: 22050, }); expect(sound).toHaveProperty('context', defaultContext); @@ -487,17 +453,15 @@ describe('Stack component', () => { // expect(sound).toHaveProperty('options.fadeMs', mockFadeMs); }); - it('registers `statechange` multi-listener on Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); - const sound = await testStack.prepare(); + it('registers `state` multi-listener on Sound', async () => { + const sound = await mockStack.prepare(); sound.play().pause(); - expect(sound.activeEvents).toContain('statechange'); + expect(sound.activeEvents).toContain('state'); }); it('registers `ended` single-listener on Sound', async () => { - const testStack = new Stack(...mockConstructorArgs); - const sound = await testStack.prepare(); + const sound = await mockStack.prepare(); sound.play(); // No way to really check that the event is removed, @@ -550,6 +514,7 @@ describe('Stack component', () => { }); }); - // Both `statechange` and `error` are covered in other tests. + // All events are covered in other tests: + // `state`, `volume`, `mute`, and `error`. // describe('events', () => {}); }); diff --git a/src/tests/helpers.test.ts b/src/tests/helpers.test.ts index 4ebc341..ac3e821 100644 --- a/src/tests/helpers.test.ts +++ b/src/tests/helpers.test.ts @@ -3,6 +3,7 @@ import {afterEach, describe, it, expect, vi} from 'vitest'; import { getErrorMessage, fetchAudioBuffer, + linearRamp, scratchBuffer, unlockAudioContext, } from '../helpers'; @@ -12,7 +13,7 @@ describe('Helpers', () => { const mockContext = new AudioContext(); describe('getErrorMessage()', () => { - it('returns message from basic object', () => { + it('returns message from basic object', async () => { const mockError = { message: 'foo', }; @@ -22,7 +23,7 @@ describe('Helpers', () => { expect(result).toBe(mockError.message); }); - it('returns message from Error', () => { + it('returns message from Error', async () => { const mockMessage = 'Foo'; const mockError = new Error(mockMessage, { cause: 'bar', @@ -33,7 +34,7 @@ describe('Helpers', () => { expect(result).toBe(mockMessage); }); - it('returns stringified result when unknown', () => { + it('returns stringified result when unknown', async () => { const mockError = ['foo', true, {bar: false}, null]; const result = getErrorMessage(mockError); @@ -45,16 +46,18 @@ describe('Helpers', () => { it('throws parse Error on bogus path', async () => { const mockPath = './path/nowhere.webm'; + // This test used to check that the error was: + // `Failed to parse URL from ${mockPath}` + // However, we now get back a `[object Request]`, await expect( async () => await fetchAudioBuffer(mockPath, mockContext), - ).rejects.toThrowError(`Failed to parse URL from ${mockPath}`); + ).rejects.toThrowError(); }); it.todo('throws network error on bad response'); - // TODO: This test might fail locally... - // We need to fix fetch requests in tests to mock a response. - it('returns AudioBuffer', async () => { + // TODO: Cannot test against `fetch()` until we correctly mock it. + it.skip('returns AudioBuffer', async () => { await expect( fetchAudioBuffer(mockData.audio, mockContext).catch((_error) => {}), ).resolves.toBeInstanceOf(AudioBuffer); @@ -63,8 +66,45 @@ describe('Helpers', () => { it.todo('passes custom options to fetch'); }); + describe('linearRamp()', () => { + it('transitions to the specified value', async () => { + const mockAudioParam = new AudioParam(); + + const spyCancel = vi.spyOn(mockAudioParam, 'cancelScheduledValues'); + const spySet = vi.spyOn(mockAudioParam, 'setValueAtTime'); + const spyRamp = vi.spyOn(mockAudioParam, 'linearRampToValueAtTime'); + + const fromValue = mockAudioParam.value; + const fromTime = mockContext.currentTime; + + const toValue = 2; + const toTime = fromTime + 2; + + expect(spyCancel).not.toBeCalled(); + expect(spySet).not.toBeCalled(); + expect(spyRamp).not.toBeCalled(); + + const result = linearRamp( + mockAudioParam, + {from: fromValue, to: toValue}, + {from: fromTime, to: toTime}, + ); + + expect(result).toBeInstanceOf(AudioParam); + + expect(spyCancel).toBeCalledTimes(1); + expect(spyCancel).toBeCalledWith(fromTime); + + expect(spySet).toBeCalledTimes(1); + expect(spySet).toBeCalledWith(fromValue, fromTime); + + expect(spyRamp).toBeCalledTimes(1); + expect(spyRamp).toBeCalledWith(toValue, toTime); + }); + }); + describe('scratchBuffer()', () => { - it('creates a short silent AudioBuffer', () => { + it('creates a short silent AudioBuffer', async () => { const result = scratchBuffer(mockContext); expect(result).toBeInstanceOf(AudioBuffer); @@ -80,7 +120,7 @@ describe('Helpers', () => { vi.advanceTimersToNextTimer(); }); - it('resumes AudioContext state', () => { + it('resumes AudioContext state', async () => { const spyCreateBuffer = vi.spyOn(mockContext, 'createBuffer'); const spyResume = vi.spyOn(mockContext, 'resume'); @@ -107,7 +147,7 @@ describe('Helpers', () => { expect(spySourceStart).toBeCalledTimes(1); }); - it('calls onEnded after interaction event', () => { + it('calls onEnded after interaction event', async () => { vi.spyOn( AudioBufferSourceNode.prototype, 'addEventListener', diff --git a/src/tests/utilities.test.ts b/src/tests/utilities.test.ts index fd209ed..074171e 100644 --- a/src/tests/utilities.test.ts +++ b/src/tests/utilities.test.ts @@ -1,6 +1,8 @@ import {describe, it, expect} from 'vitest'; import { arrayOfLength, + arrayShallowEquals, + assertNumber, clamp, progressPercentage, msToSec, @@ -10,12 +12,12 @@ import { describe('Utilities', () => { describe('Array', () => { describe('arrayOfLength()', () => { - it('returns an empty array by default', () => { + it('returns an empty array by default', async () => { const result = arrayOfLength(); expect(result).toStrictEqual([]); }); - it('returns an array of incremented index values', () => { + it('returns an array of incremented index values', async () => { const mockLength = 6; const result = arrayOfLength(mockLength); @@ -23,72 +25,117 @@ describe('Utilities', () => { expect(result).toHaveLength(mockLength); }); }); + + describe('arrayShallowEquals()', () => { + it('returns `true` when matching', async () => { + const result = arrayShallowEquals( + [true, false, null, undefined, 0, 1, 'end'], + [true, false, null, undefined, 0, 1, 'end'], + ); + expect(result).toBe(true); + }); + + it('returns `false` when at least one value is unmatched', async () => { + const result = arrayShallowEquals([true, false], [false, true]); + expect(result).toBe(false); + }); + }); }); describe('Numbers', () => { - describe('clamp()', () => { - it('returns preference', () => { - const mockArgs = { - preference: 10, - min: 1, - max: 100, - }; + describe('assertNumber()', () => { + it('accepts an integer', async () => { + const result = assertNumber(123); + expect(result).toBe(true); + }); - const result = clamp(mockArgs); + it('accepts a float', async () => { + const result = assertNumber(123.456); + expect(result).toBe(true); + }); - expect(result).toBe(mockArgs.preference); + it('does not allow a bigint', async () => { + const result = assertNumber(1000n); + expect(result).toBe(false); }); - it('returns min', () => { - const mockArgs = { - preference: 1, - min: 10, - max: 100, - }; + it('does not allow NaN', async () => { + const result = assertNumber(NaN); + expect(result).toBe(false); + }); - const result = clamp(mockArgs); + it('does not allow Infinity', async () => { + const result = assertNumber(Infinity); + expect(result).toBe(false); + }); - expect(result).toBe(mockArgs.min); + it('does not allow other non-numnber types', async () => { + expect(assertNumber(true)).toBe(false); + expect(assertNumber(false)).toBe(false); + expect(assertNumber({one: 1})).toBe(false); + expect(assertNumber([1, 2, 3])).toBe(false); + expect(assertNumber('123')).toBe(false); }); + }); - it('returns max', () => { - const mockArgs = { - preference: 100, - min: 1, - max: 10, - }; + describe('clamp()', () => { + it('returns preference', async () => { + const mockArgs: Parameters = [1, 10, 100]; + const result = clamp(...mockArgs); + + expect(result).toBe(10); + }); + + it('returns min', async () => { + const mockArgs: Parameters = [10, 1, 100]; + const result = clamp(...mockArgs); + + expect(result).toBe(10); + }); - const result = clamp(mockArgs); + it('returns max', async () => { + const mockArgs: Parameters = [1, 100, 10]; + const result = clamp(...mockArgs); - expect(result).toBe(mockArgs.max); + expect(result).toBe(10); }); }); describe('progressPercentage()', () => { - it('returns percentage integer', () => { + it('returns percentage integer', async () => { const result = progressPercentage(6, 20); expect(result).toBe(30); }); - it('round down floating-point result', () => { + it('round down floating-point result', async () => { const result = progressPercentage(12, 345); // Before Math.floor(), result should be: // 3.4782608695652173 expect(result).toBe(3); }); + + it('protects against NaN', async () => { + const result = progressPercentage(0, 0); + expect(result).toBe(0); + }); + + it('protects against Infinity', async () => { + const result = progressPercentage(2, 0); + expect(result).toBe(0); + }); }); }); describe('Time', () => { describe('msToSec()', () => { - it('converts to seconds', () => { + it('converts to seconds', async () => { const result = msToSec(1234); expect(result).toBe(1.234); }); }); describe('secToMs()', () => { - it('converts to milliseconds', () => { + it('converts to milliseconds', async () => { const result = secToMs(5.678); expect(result).toBe(5678); }); diff --git a/src/tokens.ts b/src/tokens.ts index 061c903..ab5c8ad 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,6 +1,9 @@ export const tokens = { maxStackSize: 8, suspendAfterMs: 30000, - // Browser's don't seem to accept a `0` value. - minPlaybackRate: 0.0001, + minStartTime: 0.0001, + minSpeed: 0.25, + maxSpeed: 4, + // FireFox does not accept a `0` value. + pauseSpeed: 0.0001, }; diff --git a/src/types.ts b/src/types.ts index 83c0fd0..4f13aef 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,12 @@ +export type PrimitiveType = + | string + | number + | bigint + | boolean + | symbol + | undefined + | null; + export type TimeoutId = number | ReturnType; // Tuple: custom error, original error. @@ -10,8 +19,11 @@ export type ManagerState = AudioContextState | 'suspending' | 'interrupted'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type ManagerEventMap = { - statechange: (state: ManagerState) => void; - error: (error: CombinedErrorMessage) => void; + state: (current: ManagerState) => void; + library: (newKeys: StackId[], oldKeys: StackId[]) => void; + volume: (level: number) => void; + mute: (muted: boolean) => void; + error: (messages: CombinedErrorMessage) => void; }; export interface ManagerConfig { @@ -25,8 +37,6 @@ export interface LibraryEntry { path: string; } -export type LibraryKeys = StackId[]; - /// /// Stack @@ -40,8 +50,11 @@ export interface StackError { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type StackEventMap = { - statechange: (state: StackState) => void; - error: (error: StackError) => void; + state: (current: StackState) => void; + queue: (newKeys: SoundId[], oldKeys: SoundId[]) => void; + volume: (level: number) => void; + mute: (muted: boolean) => void; + error: (message: StackError) => void; }; export interface StackConfig { @@ -56,17 +69,34 @@ export interface StackConfig { export type SoundId = string; // TODO: Are there any errors that can occur on a `Sound`? // If so, we need to add an error `event` and/or `state`. -export type SoundState = 'created' | 'playing' | 'paused' | 'stopping'; +export type SoundState = + | 'created' + | 'playing' + | 'paused' + | 'stopping' + | 'ending'; export interface SoundEndedEvent { id: SoundId; source: AudioBufferSourceNode; + neverStarted: boolean; +} + +export interface SoundProgressEvent { + elapsed: number; + remaining: number; + percentage: number; + iterations: number; } // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type SoundEventMap = { - statechange: (state: SoundState) => void; + state: (current: SoundState) => void; ended: (event: SoundEndedEvent) => void; + volume: (level: number) => void; + mute: (muted: boolean) => void; + speed: (rate: number) => void; + progress: (event: SoundProgressEvent) => void; // loop(ended: boolean): void; }; diff --git a/src/utilities/array.ts b/src/utilities/array.ts index d5af360..da64bca 100644 --- a/src/utilities/array.ts +++ b/src/utilities/array.ts @@ -1,3 +1,10 @@ +import type {PrimitiveType} from '../types'; + export function arrayOfLength(length = 0): number[] { return Array.from(Array(length)).map((_item, index) => index); } + +export function arrayShallowEquals(one: PrimitiveType[], two: PrimitiveType[]) { + const equalLength = one.length === two.length; + return equalLength && one.every((value, index) => value === two[index]); +} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index cfcb7c2..9c91b30 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,3 @@ -export {arrayOfLength} from './array'; -export {clamp, progressPercentage} from './numbers'; +export {arrayOfLength, arrayShallowEquals} from './array'; +export {assertNumber, clamp, progressPercentage} from './numbers'; export {msToSec, secToMs} from './time'; diff --git a/src/utilities/numbers.ts b/src/utilities/numbers.ts index 95f44f3..4c602b6 100644 --- a/src/utilities/numbers.ts +++ b/src/utilities/numbers.ts @@ -1,13 +1,14 @@ -interface ClampCriteria { - preference: number; - min: number; - max: number; +export function assertNumber(value?: unknown): value is number { + // Both `NaN` and `Infinity` are of type `number`, + // but will not pass the `isFinite()` check. + return typeof value === 'number' && isFinite(value); } -export function clamp({preference, min, max}: ClampCriteria) { +export function clamp(min = 0, preference = 0.5, max = 1) { return Math.min(Math.max(min, preference), max); } -export function progressPercentage(value: number, max: number) { - return Math.floor((value / max) * 100); +export function progressPercentage(value = 0, max = 100) { + const progress = Math.floor((value / max) * 100); + return assertNumber(progress) ? progress : 0; }