From bf9aa1858dab2e1bca3be390ce2392b99d208dbf Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 13 Oct 2022 23:20:36 +0300 Subject: [PATCH] feat: @discordjs/brokers (#8548) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/labeler.yml | 3 + .github/labels.yml | 2 + .github/workflows/documentation.yml | 2 +- .github/workflows/npm-auto-deprecate.yml | 2 +- .github/workflows/publish-dev.yml | 2 + apps/website/src/util/constants.ts | 2 +- .../actions/src/uploadCoverage/action.yml | 6 + packages/brokers/.cliff-jumperrc.json | 5 + packages/brokers/.eslintrc.json | 3 + packages/brokers/.gitignore | 27 +++ packages/brokers/.lintstagedrc.js | 1 + packages/brokers/.prettierignore | 8 + packages/brokers/.prettierrc.js | 1 + packages/brokers/LICENSE | 191 ++++++++++++++++++ packages/brokers/README.md | 120 +++++++++++ packages/brokers/__tests__/index.test.ts | 18 ++ packages/brokers/api-extractor.json | 3 + packages/brokers/cliff.toml | 63 ++++++ packages/brokers/docs/README.md | 1 + packages/brokers/docs/index.json | 1 + packages/brokers/package.json | 82 ++++++++ packages/brokers/scripts/xcleangroup.lua | 16 ++ packages/brokers/src/brokers/Broker.ts | 86 ++++++++ .../brokers/src/brokers/redis/BaseRedis.ts | 172 ++++++++++++++++ .../brokers/src/brokers/redis/PubSubRedis.ts | 58 ++++++ .../brokers/src/brokers/redis/RPCRedis.ts | 130 ++++++++++++ packages/brokers/src/index.ts | 5 + packages/brokers/tsconfig.eslint.json | 20 ++ packages/brokers/tsconfig.json | 4 + packages/brokers/tsup.config.js | 3 + yarn.lock | 186 ++++++++++++++++- 32 files changed, 1210 insertions(+), 14 deletions(-) create mode 100644 packages/brokers/.cliff-jumperrc.json create mode 100644 packages/brokers/.eslintrc.json create mode 100644 packages/brokers/.gitignore create mode 100644 packages/brokers/.lintstagedrc.js create mode 100644 packages/brokers/.prettierignore create mode 100644 packages/brokers/.prettierrc.js create mode 100644 packages/brokers/LICENSE create mode 100644 packages/brokers/README.md create mode 100644 packages/brokers/__tests__/index.test.ts create mode 100644 packages/brokers/api-extractor.json create mode 100644 packages/brokers/cliff.toml create mode 100644 packages/brokers/docs/README.md create mode 100644 packages/brokers/docs/index.json create mode 100644 packages/brokers/package.json create mode 100644 packages/brokers/scripts/xcleangroup.lua create mode 100644 packages/brokers/src/brokers/Broker.ts create mode 100644 packages/brokers/src/brokers/redis/BaseRedis.ts create mode 100644 packages/brokers/src/brokers/redis/PubSubRedis.ts create mode 100644 packages/brokers/src/brokers/redis/RPCRedis.ts create mode 100644 packages/brokers/src/index.ts create mode 100644 packages/brokers/tsconfig.eslint.json create mode 100644 packages/brokers/tsconfig.json create mode 100644 packages/brokers/tsup.config.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f800fbb26..e356e097d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,6 +12,7 @@ body: label: Which package is this bug report for? options: - discord.js + - brokers - builders - collection - rest diff --git a/.github/labeler.yml b/.github/labeler.yml index 8d5581374..9424ed106 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,6 +5,9 @@ apps:website: - apps/website/* - apps/website/**/* +packages:brokers: + - packages/brokers/* + - packages/brokers/**/* packages:builders: - packages/builders/* - packages/builders/**/* diff --git a/.github/labels.yml b/.github/labels.yml index 9ba45fda4..8bc0a7d3b 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -50,6 +50,8 @@ color: e4e669 - name: need repro color: c66037 +- name: packages:brokers + color: fbca04 - name: packages:builders color: fbca04 - name: packages:collection diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 98852fb9a..31c7875b7 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -68,7 +68,7 @@ jobs: max-parallel: 1 fail-fast: false matrix: - package: ['builders', 'collection', 'discord.js', 'proxy', 'rest', 'util', 'voice', 'ws'] + package: ['brokers', 'builders', 'collection', 'discord.js', 'proxy', 'rest', 'util', 'voice', 'ws'] runs-on: ubuntu-latest env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} diff --git a/.github/workflows/npm-auto-deprecate.yml b/.github/workflows/npm-auto-deprecate.yml index 2acc083f7..9774b2f25 100644 --- a/.github/workflows/npm-auto-deprecate.yml +++ b/.github/workflows/npm-auto-deprecate.yml @@ -22,6 +22,6 @@ jobs: run: yarn --immutable - name: Deprecate versions - run: 'yarn npm-deprecate --name "*dev*" --package @discordjs/builders @discordjs/collection discord.js @discordjs/proxy @discordjs/rest @discordjs/util @discordjs/voice @discordjs/ws' + run: 'yarn npm-deprecate --name "*dev*" --package @discordjs/brokers @discordjs/builders @discordjs/collection discord.js @discordjs/proxy @discordjs/rest @discordjs/util @discordjs/voice @discordjs/ws' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index fc0a28db6..ea51cee9b 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -10,6 +10,8 @@ jobs: fail-fast: false matrix: include: + - package: '@discordjs/brokers' + folder: 'brokers' - package: '@discordjs/builders' folder: 'builders' - package: '@discordjs/collection' diff --git a/apps/website/src/util/constants.ts b/apps/website/src/util/constants.ts index b15deb233..bc85a14f3 100644 --- a/apps/website/src/util/constants.ts +++ b/apps/website/src/util/constants.ts @@ -1,4 +1,4 @@ -export const PACKAGES = ['builders', 'collection', 'proxy', 'rest', 'util', 'voice', 'ws']; +export const PACKAGES = ['brokers', 'builders', 'collection', 'proxy', 'rest', 'util', 'voice', 'ws']; export const DESCRIPTION = "discord.js is a powerful node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend."; diff --git a/packages/actions/src/uploadCoverage/action.yml b/packages/actions/src/uploadCoverage/action.yml index ec42c5dbc..f45d0e70b 100644 --- a/packages/actions/src/uploadCoverage/action.yml +++ b/packages/actions/src/uploadCoverage/action.yml @@ -15,6 +15,12 @@ runs: files: ./apps/website/coverage/cobertura-coverage.xml flags: website + - name: Upload Brokers Coverage + uses: codecov/codecov-action@v3 + with: + files: ./packages/brokers/coverage/cobertura-coverage.xml + flags: brokers + - name: Upload Builders Coverage uses: codecov/codecov-action@v3 with: diff --git a/packages/brokers/.cliff-jumperrc.json b/packages/brokers/.cliff-jumperrc.json new file mode 100644 index 000000000..819afd40a --- /dev/null +++ b/packages/brokers/.cliff-jumperrc.json @@ -0,0 +1,5 @@ +{ + "name": "brokers", + "org": "discordjs", + "packagePath": "packages/brokers" +} diff --git a/packages/brokers/.eslintrc.json b/packages/brokers/.eslintrc.json new file mode 100644 index 000000000..99ef7cec8 --- /dev/null +++ b/packages/brokers/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/brokers/.gitignore b/packages/brokers/.gitignore new file mode 100644 index 000000000..86b93e929 --- /dev/null +++ b/packages/brokers/.gitignore @@ -0,0 +1,27 @@ +# Packages +node_modules/ + +# Log files +logs/ +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Env +.env + +# Dist +dist/ +typings/ +docs/**/* +!docs/index.json +!docs/README.md + +# Miscellaneous +.tmp/ +coverage/ +tsconfig.tsbuildinfo diff --git a/packages/brokers/.lintstagedrc.js b/packages/brokers/.lintstagedrc.js new file mode 100644 index 000000000..dc17706a5 --- /dev/null +++ b/packages/brokers/.lintstagedrc.js @@ -0,0 +1 @@ +module.exports = require('../../.lintstagedrc.json'); diff --git a/packages/brokers/.prettierignore b/packages/brokers/.prettierignore new file mode 100644 index 000000000..8b94c7d45 --- /dev/null +++ b/packages/brokers/.prettierignore @@ -0,0 +1,8 @@ +# Autogenerated +CHANGELOG.md +.turbo +dist/ +docs/**/* +!docs/index.yml +!docs/README.md +coverage/ \ No newline at end of file diff --git a/packages/brokers/.prettierrc.js b/packages/brokers/.prettierrc.js new file mode 100644 index 000000000..f004026c7 --- /dev/null +++ b/packages/brokers/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../.prettierrc.json'); diff --git a/packages/brokers/LICENSE b/packages/brokers/LICENSE new file mode 100644 index 000000000..f9786ff8f --- /dev/null +++ b/packages/brokers/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 Noel Buechler + Copyright 2022 Charlotte Cristea + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/brokers/README.md b/packages/brokers/README.md new file mode 100644 index 000000000..33b7e60fb --- /dev/null +++ b/packages/brokers/README.md @@ -0,0 +1,120 @@ +
+
+

+ discord.js +

+
+

+ Discord server + npm version + npm downloads + Build status + Code coverage +

+

+ Vercel +

+
+ +## About + +`@discordjs/brokers` is a powerful set of message brokers + +## Installation + +**Node.js 16.9.0 or newer is required.** + +```sh-session +npm install @discordjs/brokers +yarn add @discordjs/brokers +pnpm add @discordjs/brokers +``` + +## Example usage + +### pub sub + +```ts +// publisher.js +import { PubSubRedisBroker } from '@discordjs/brokers'; +import Redis from 'ioredis'; + +const broker = new PubSubRedisBroker({ redisClient: new Redis() }); + +await broker.publish('test', 'Hello World!'); +await broker.destroy(); + +// subscriber.js +import { PubSubRedisBroker } from '@discordjs/brokers'; +import Redis from 'ioredis'; + +const broker = new PubSubRedisBroker({ redisClient: new Redis() }); +broker.on('test', ({ data, ack }) => { + console.log(data); + void ack(); +}); + +await broker.subscribe('subscribers', ['test']); +``` + +### RPC + +```ts +// caller.js +import { RPCRedisBroker } from '@discordjs/brokers'; +import Redis from 'ioredis'; + +const broker = new RPCRedisBroker({ redisClient: new Redis() }); + +console.log(await broker.call('testcall', 'Hello World!')); +await broker.destroy(); + +// responder.js +import { RPCRedisBroker } from '@discordjs/brokers'; +import Redis from 'ioredis'; + +const broker = new RPCRedisBroker({ redisClient: new Redis() }); +broker.on('testcall', ({ data, ack, reply }) => { + console.log('responder', data); + void ack(); + void reply(`Echo: ${data}`); +}); + +await broker.subscribe('responders', ['testcall']); +``` + +## Links + +- [Website][website] ([source][website-source]) +- [Documentation][documentation] +- [Guide][guide] ([source][guide-source]) + See also the [Update Guide][guide-update], including updated and removed items in the library. +- [discord.js Discord server][discord] +- [Discord API Discord server][discord-api] +- [GitHub][source] +- [npm][npm] +- [Related libraries][related-libs] + +## Contributing + +Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the +[documentation][documentation]. +See [the contribution guide][contributing] if you'd like to submit a PR. + +## Help + +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle +nudge in the right direction, please don't hesitate to join our official [discord.js Server][discord]. + +[website]: https://discord.js.org/ +[website-source]: https://github.com/discordjs/discord.js/tree/main/apps/website +[documentation]: https://discord.js.org/#/docs/brokers +[guide]: https://discordjs.guide/ +[guide-source]: https://github.com/discordjs/guide +[guide-update]: https://discordjs.guide/additional-info/changes-in-v14.html +[discord]: https://discord.gg/djs +[discord-api]: https://discord.gg/discord-api +[source]: https://github.com/discordjs/discord.js/tree/main/packages/brokers +[npm]: https://www.npmjs.com/package/@discordjs/brokers +[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries +[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md diff --git a/packages/brokers/__tests__/index.test.ts b/packages/brokers/__tests__/index.test.ts new file mode 100644 index 000000000..e133c58d9 --- /dev/null +++ b/packages/brokers/__tests__/index.test.ts @@ -0,0 +1,18 @@ +import type Redis from 'ioredis'; +import { test, expect, vi } from 'vitest'; +import { PubSubRedisBroker } from '../src/index.js'; + +const mockRedisClient = { + defineCommand: vi.fn(), + xadd: vi.fn(), + duplicate: vi.fn(() => mockRedisClient), +} as unknown as Redis; + +test('pubsub with custom encoding', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const encode = vi.fn((data) => data); + + const broker = new PubSubRedisBroker({ redisClient: mockRedisClient, encode }); + await broker.publish('test', 'test'); + expect(encode).toHaveBeenCalledWith('test'); +}); diff --git a/packages/brokers/api-extractor.json b/packages/brokers/api-extractor.json new file mode 100644 index 000000000..bc73f2cc0 --- /dev/null +++ b/packages/brokers/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../api-extractor.json" +} diff --git a/packages/brokers/cliff.toml b/packages/brokers/cliff.toml new file mode 100644 index 000000000..56402e3f6 --- /dev/null +++ b/packages/brokers/cliff.toml @@ -0,0 +1,63 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + (https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + (https://github.com/discordjs/discord.js/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/discordjs/discord.js/commit/{{ commit.id }}))\ + {% if commit.breaking %}\ + {% for breakingChange in commit.footers %}\ + \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^typings", group = "Typings"}, + { message = "^types", group = "Typings"}, + { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"}, + { message = "^revert", skip = true}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore", skip = true}, + { message = "^ci", skip = true}, + { message = "^build", skip = true}, + { body = ".*security", group = "Security"}, +] +filter_commits = true +tag_pattern = "@discordjs/brokers@[0-9]*" +ignore_tags = "" +date_order = true +sort_commits = "newest" diff --git a/packages/brokers/docs/README.md b/packages/brokers/docs/README.md new file mode 100644 index 000000000..88e2e10f7 --- /dev/null +++ b/packages/brokers/docs/README.md @@ -0,0 +1 @@ +## [View the documentation here.](https://discord.js.org/#/docs/brokers) diff --git a/packages/brokers/docs/index.json b/packages/brokers/docs/index.json new file mode 100644 index 000000000..557341ae9 --- /dev/null +++ b/packages/brokers/docs/index.json @@ -0,0 +1 @@ +[{ "name": "General", "files": [{ "name": "Welcome", "id": "welcome", "path": "../../README.md" }] }] diff --git a/packages/brokers/package.json b/packages/brokers/package.json new file mode 100644 index 000000000..c6c988600 --- /dev/null +++ b/packages/brokers/package.json @@ -0,0 +1,82 @@ +{ + "name": "@discordjs/brokers", + "version": "0.1.0", + "description": "Powerful set of message brokers", + "scripts": { + "test": "vitest run", + "build": "tsup", + "lint": "prettier --check . && cross-env TIMING=1 eslint src __tests__ --ext .mjs,.js,.ts --format=pretty", + "format": "prettier --write . && cross-env TIMING=1 eslint src __tests__ --ext .mjs,.js,.ts --fix --format=pretty", + "fmt": "yarn format", + "docs": "api-extractor run --local", + "prepack": "yarn lint && yarn test && yarn build", + "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/brokers/*'", + "release": "cliff-jumper" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "typings": "./dist/index.d.ts", + "exports": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "directories": { + "lib": "src", + "test": "__tests__" + }, + "files": [ + "dist" + ], + "contributors": [ + "Crawl ", + "Amish Shah ", + "SpaceEEC ", + "Vlad Frangu ", + "Aura Roman ", + "DD " + ], + "license": "Apache-2.0", + "keywords": [ + "discord", + "api", + "message", + "brokers", + "redis", + "discordapp", + "discordjs" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/discordjs/discord.js.git" + }, + "bugs": { + "url": "https://github.com/discordjs/discord.js/issues" + }, + "homepage": "https://discord.js.org", + "dependencies": { + "@msgpack/msgpack": "^2.8.0", + "@vladfrangu/async_event_emitter": "^2.1.2", + "ioredis": "^5.2.3" + }, + "devDependencies": { + "@favware/cliff-jumper": "^1.8.8", + "@microsoft/api-extractor": "^7.32.1", + "@types/node": "^16.11.52", + "@vitest/coverage-c8": "^0.22.1", + "cross-env": "^7.0.3", + "eslint": "^8.25.0", + "eslint-config-neon": "^0.1.38", + "eslint-formatter-pretty": "^4.1.0", + "prettier": "^2.7.1", + "tsup": "^6.2.3", + "typescript": "^4.8.4", + "vitest": "^0.22.1" + }, + "engines": { + "node": ">=16.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/brokers/scripts/xcleangroup.lua b/packages/brokers/scripts/xcleangroup.lua new file mode 100644 index 000000000..7553a56b5 --- /dev/null +++ b/packages/brokers/scripts/xcleangroup.lua @@ -0,0 +1,16 @@ +local info = redis.call('XINFO', 'CONSUMERS', KEYS[1], ARGS[1]) +local empty = true + +for k, consumer in pairs(info) do + if consumer['idle'] != 0 then + empty = false + break + end +end + +if empty then + redis.call('XGROUP', 'DESTROY', KEYS[1], ARGS[1]) + return true +end + +return false diff --git a/packages/brokers/src/brokers/Broker.ts b/packages/brokers/src/brokers/Broker.ts new file mode 100644 index 000000000..40c31f3b2 --- /dev/null +++ b/packages/brokers/src/brokers/Broker.ts @@ -0,0 +1,86 @@ +import { Buffer } from 'node:buffer'; +import { randomBytes } from 'node:crypto'; +import { encode, decode } from '@msgpack/msgpack'; +import type { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; + +/** + * Base options for a broker implementation + */ +export interface BaseBrokerOptions { + /** + * How long to block for messages when polling + */ + blockTimeout?: number; + /** + * Function to use for decoding messages + */ + // eslint-disable-next-line @typescript-eslint/method-signature-style + decode?: (data: Buffer) => unknown; + /** + * Function to use for encoding messages + */ + // eslint-disable-next-line @typescript-eslint/method-signature-style + encode?: (data: unknown) => Buffer; + /** + * Max number of messages to poll at once + */ + maxChunk?: number; + /** + * Unique consumer name. See: https://redis.io/commands/xreadgroup/ + */ + name?: string; +} + +/** + * Default broker options + */ +export const DefaultBrokerOptions: Required = { + name: randomBytes(20).toString('hex'), + maxChunk: 10, + blockTimeout: 5_000, + encode: (data): Buffer => { + const encoded = encode(data); + return Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); + }, + decode: (data): unknown => decode(data), +}; + +export type ToEventMap< + TRecord extends Record, + TResponses extends Record | undefined = undefined, +> = { + [TKey in keyof TRecord]: [ + event: TResponses extends Record + ? { ack(): Promise; reply(data: TResponses[TKey]): Promise } + : { ack(): Promise } & { data: TRecord[TKey] }, + ]; +} & { [K: string]: any }; + +export interface IBaseBroker> { + /** + * Subscribes to the given events, grouping them by the given group name + */ + subscribe(group: string, events: (keyof TEvents)[]): Promise; + /** + * Unsubscribes from the given events - it's required to pass the same group name as when subscribing for proper cleanup + */ + unsubscribe(group: string, events: (keyof TEvents)[]): Promise; +} + +export interface IPubSubBroker> + extends IBaseBroker, + AsyncEventEmitter> { + /** + * Publishes an event + */ + publish(event: T, data: TEvents[T]): Promise; +} + +export interface IRPCBroker, TResponses extends Record> + extends IBaseBroker, + AsyncEventEmitter> { + /** + * Makes an RPC call + */ + call(event: T, data: TEvents[T], timeoutDuration?: number): Promise; +} diff --git a/packages/brokers/src/brokers/redis/BaseRedis.ts b/packages/brokers/src/brokers/redis/BaseRedis.ts new file mode 100644 index 000000000..a075c0a47 --- /dev/null +++ b/packages/brokers/src/brokers/redis/BaseRedis.ts @@ -0,0 +1,172 @@ +import type { Buffer } from 'node:buffer'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; +import type { Redis } from 'ioredis'; +import { ReplyError } from 'ioredis'; +import type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js'; +import { DefaultBrokerOptions } from '../Broker.js'; + +// For some reason ioredis doesn't have this typed, but it exists +declare module 'ioredis' { + interface Redis { + xreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>; + } +} + +/** + * Options specific for a Redis broker + */ +export interface RedisBrokerOptions extends BaseBrokerOptions { + /** + * The Redis client to use + */ + redisClient: Redis; +} + +/** + * Helper class with shared Redis logic + */ +export abstract class BaseRedisBroker> + extends AsyncEventEmitter> + implements IBaseBroker +{ + /** + * Used for Redis queues, see the 3rd argument taken by {@link https://redis.io/commands/xadd | xadd } + */ + public static readonly STREAM_DATA_KEY = 'data'; + + /** + * Options this broker is using + */ + protected readonly options: Required; + + /** + * Events this broker has subscribed to + */ + protected readonly subscribedEvents = new Set(); + + /** + * Internal copy of the Redis client being used to read incoming payloads + */ + protected readonly streamReadClient: Redis; + + /** + * Whether this broker is currently polling events + */ + protected listening = false; + + public constructor(options: RedisBrokerOptions) { + super(); + this.options = { ...DefaultBrokerOptions, ...options }; + options.redisClient.defineCommand('xcleangroup', { + numberOfKeys: 1, + lua: readFileSync(resolve(__dirname, '..', '..', '..', 'scripts', 'xcleangroup.lua'), 'utf8'), + }); + this.streamReadClient = options.redisClient.duplicate(); + } + + /** + * {@inheritDoc IBaseBroker.subscribe} + */ + public async subscribe(group: string, events: (keyof TEvents)[]): Promise { + await Promise.all( + // eslint-disable-next-line consistent-return + events.map(async (event) => { + this.subscribedEvents.add(event as string); + try { + return await this.options.redisClient.xgroup('CREATE', event as string, group, 0, 'MKSTREAM'); + } catch (error) { + if (!(error instanceof ReplyError)) { + throw error; + } + } + }), + ); + void this.listen(group); + } + + /** + * {@inheritDoc IBaseBroker.unsubscribe} + */ + public async unsubscribe(group: string, events: (keyof TEvents)[]): Promise { + const commands: unknown[][] = Array.from({ length: events.length * 2 }); + for (let idx = 0; idx < commands.length; idx += 2) { + const event = events[idx / 2]; + commands[idx] = ['xgroup', 'delconsumer', event as string, group, this.options.name]; + commands[idx + 1] = ['xcleangroup', event as string, group]; + } + + await this.options.redisClient.pipeline(commands).exec(); + + for (const event of events) { + this.subscribedEvents.delete(event as string); + } + } + + /** + * Begins polling for events, firing them to {@link BaseRedisBroker.listen} + */ + protected async listen(group: string): Promise { + if (this.listening) { + return; + } + + this.listening = true; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + try { + const data = await this.streamReadClient.xreadgroupBuffer( + 'GROUP', + group, + this.options.name, + 'COUNT', + String(this.options.maxChunk), + 'BLOCK', + String(this.options.blockTimeout), + 'STREAMS', + ...this.subscribedEvents, + ...Array.from({ length: this.subscribedEvents.size }, () => '>'), + ); + + if (!data) { + continue; + } + + for (const [event, info] of data) { + for (const [id, packet] of info) { + const idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0); + if (idx < 0) { + continue; + } + + const data = packet[idx + 1]; + if (!data) { + continue; + } + + this.emitEvent(id, group, event.toString('utf8'), this.options.decode(data)); + } + } + } catch (error) { + this.emit('error', error); + break; + } + } + + this.listening = false; + } + + /** + * Destroys the broker, closing all connections + */ + public async destroy() { + this.streamReadClient.disconnect(); + this.options.redisClient.disconnect(); + } + + /** + * Handles an incoming Redis event + */ + protected abstract emitEvent(id: Buffer, group: string, event: string, data: unknown): unknown; +} diff --git a/packages/brokers/src/brokers/redis/PubSubRedis.ts b/packages/brokers/src/brokers/redis/PubSubRedis.ts new file mode 100644 index 000000000..79b0b41fc --- /dev/null +++ b/packages/brokers/src/brokers/redis/PubSubRedis.ts @@ -0,0 +1,58 @@ +import type { Buffer } from 'node:buffer'; +import type { IPubSubBroker } from '../Broker.js'; +import { BaseRedisBroker } from './BaseRedis.js'; + +/** + * PubSub broker powered by Redis + * + * @example + * ```ts + * // publisher.js + * import { PubSubRedisBroker } from '@discordjs/brokers'; + * import Redis from 'ioredis'; + * + * const broker = new PubSubRedisBroker({ redisClient: new Redis() }); + * + * await broker.publish('test', 'Hello World!'); + * await broker.destroy(); + * + * // subscriber.js + * import { PubSubRedisBroker } from '@discordjs/brokers'; + * import Redis from 'ioredis'; + * + * const broker = new PubSubRedisBroker({ redisClient: new Redis() }); + * broker.on('test', ({ data, ack }) => { + * console.log(data); + * void ack(); + * }); + * + * await broker.subscribe('subscribers', ['test']); + * ``` + */ +export class PubSubRedisBroker> + extends BaseRedisBroker + implements IPubSubBroker +{ + /** + * {@inheritDoc IPubSubBroker.publish} + */ + public async publish(event: T, data: TEvents[T]): Promise { + await this.options.redisClient.xadd( + event as string, + '*', + BaseRedisBroker.STREAM_DATA_KEY, + this.options.encode(data), + ); + } + + protected emitEvent(id: Buffer, group: string, event: string, data: unknown) { + const payload: { ack(): Promise; data: unknown } = { + data, + ack: async () => { + await this.options.redisClient.xack(event, group, id); + }, + }; + + this.emit(event, payload); + } +} diff --git a/packages/brokers/src/brokers/redis/RPCRedis.ts b/packages/brokers/src/brokers/redis/RPCRedis.ts new file mode 100644 index 000000000..5aed520d9 --- /dev/null +++ b/packages/brokers/src/brokers/redis/RPCRedis.ts @@ -0,0 +1,130 @@ +import type { Buffer } from 'node:buffer'; +import { clearTimeout, setTimeout } from 'node:timers'; +import type { IRPCBroker } from '../Broker.js'; +import { DefaultBrokerOptions } from '../Broker.js'; +import type { RedisBrokerOptions } from './BaseRedis.js'; +import { BaseRedisBroker } from './BaseRedis.js'; + +interface InternalPromise { + reject(error: any): void; + resolve(data: any): void; + timeout: NodeJS.Timeout; +} + +/** + * Options specific for an RPC Redis broker + */ +export interface RPCRedisBrokerOptions extends RedisBrokerOptions { + timeout?: number; +} + +/** + * Default values used for the {@link RPCRedisBrokerOptions} + */ +export const DefaultRPCRedisBrokerOptions: Required> = { + ...DefaultBrokerOptions, + timeout: 5_000, +}; + +/** + * RPC broker powered by Redis + * + * @example + * ```ts + * // caller.js + * import { RPCRedisBroker } from '@discordjs/brokers'; + * import Redis from 'ioredis'; + * + * const broker = new RPCRedisBroker({ redisClient: new Redis() }); + * + * console.log(await broker.call('testcall', 'Hello World!')); + * await broker.destroy(); + * + * // responder.js + * import { RPCRedisBroker } from '@discordjs/brokers'; + * import Redis from 'ioredis'; + * + * const broker = new RPCRedisBroker({ redisClient: new Redis() }); + * broker.on('testcall', ({ data, ack, reply }) => { + * console.log('responder', data); + * void ack(); + * void reply(`Echo: ${data}`); + * }); + * + * await broker.subscribe('responders', ['testcall']); + * ``` + */ +export class RPCRedisBroker, TResponses extends Record> + extends BaseRedisBroker + implements IRPCBroker +{ + /** + * Options this broker is using + */ + protected override readonly options: Required; + + protected readonly promises = new Map(); + + public constructor(options: RPCRedisBrokerOptions) { + super(options); + this.options = { ...DefaultRPCRedisBrokerOptions, ...options }; + + this.streamReadClient.on('messageBuffer', (channel: Buffer, message: Buffer) => { + const [, id] = channel.toString().split(':'); + if (id && this.promises.has(id)) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { resolve, timeout } = this.promises.get(id)!; + resolve(this.options.decode(message)); + clearTimeout(timeout); + } + }); + } + + /** + * {@inheritDoc IRPCBroker.call} + */ + public async call( + event: T, + data: TEvents[T], + timeoutDuration: number = this.options.timeout, + ): Promise { + const id = await this.options.redisClient.xadd( + event as string, + '*', + BaseRedisBroker.STREAM_DATA_KEY, + this.options.encode(data), + ); + // This id! assertion is valid. From redis docs: + // "The command returns a Null reply when used with the NOMKSTREAM option and the key doesn't exist." + // See: https://redis.io/commands/xadd/ + const rpcChannel = `${event as string}:${id!}`; + + // Construct the error here for better stack traces + const timedOut = new Error(`timed out after ${timeoutDuration}ms`); + + await this.streamReadClient.subscribe(rpcChannel); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(timedOut), timeoutDuration).unref(); + + this.promises.set(id!, { resolve, reject, timeout }); + // eslint-disable-next-line promise/prefer-await-to-then + }).finally(() => { + void this.streamReadClient.unsubscribe(rpcChannel); + this.promises.delete(id!); + }); + } + + protected emitEvent(id: Buffer, group: string, event: string, data: unknown) { + const payload: { ack(): Promise; data: unknown; reply(data: unknown): Promise } = { + data, + ack: async () => { + await this.options.redisClient.xack(event, group, id); + }, + reply: async (data) => { + await this.options.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data)); + }, + }; + + this.emit(event, payload); + } +} diff --git a/packages/brokers/src/index.ts b/packages/brokers/src/index.ts new file mode 100644 index 000000000..e421e5c46 --- /dev/null +++ b/packages/brokers/src/index.ts @@ -0,0 +1,5 @@ +export * from './brokers/redis/BaseRedis.js'; +export * from './brokers/redis/PubSubRedis.js'; +export * from './brokers/redis/RPCRedis.js'; + +export * from './brokers/Broker.js'; diff --git a/packages/brokers/tsconfig.eslint.json b/packages/brokers/tsconfig.eslint.json new file mode 100644 index 000000000..d04d4be3a --- /dev/null +++ b/packages/brokers/tsconfig.eslint.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.mjs", + "**/*.jsx", + "**/*.test.ts", + "**/*.test.js", + "**/*.test.mjs", + "**/*.spec.ts", + "**/*.spec.js", + "**/*.spec.mjs" + ], + "exclude": [] +} diff --git a/packages/brokers/tsconfig.json b/packages/brokers/tsconfig.json new file mode 100644 index 000000000..fd8b5e417 --- /dev/null +++ b/packages/brokers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/brokers/tsup.config.js b/packages/brokers/tsup.config.js new file mode 100644 index 000000000..2e679fd0a --- /dev/null +++ b/packages/brokers/tsup.config.js @@ -0,0 +1,3 @@ +import { createTsupConfig } from '../../tsup.config.js'; + +export default createTsupConfig(); diff --git a/yarn.lock b/yarn.lock index 85fcbd714..d26b8f365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2028,6 +2028,28 @@ __metadata: languageName: unknown linkType: soft +"@discordjs/brokers@workspace:packages/brokers": + version: 0.0.0-use.local + resolution: "@discordjs/brokers@workspace:packages/brokers" + dependencies: + "@favware/cliff-jumper": ^1.8.8 + "@microsoft/api-extractor": ^7.32.1 + "@msgpack/msgpack": ^2.8.0 + "@types/node": ^16.11.52 + "@vitest/coverage-c8": ^0.22.1 + "@vladfrangu/async_event_emitter": ^2.1.2 + cross-env: ^7.0.3 + eslint: ^8.25.0 + eslint-config-neon: ^0.1.38 + eslint-formatter-pretty: ^4.1.0 + ioredis: ^5.2.3 + prettier: ^2.7.1 + tsup: ^6.2.3 + typescript: ^4.8.4 + vitest: ^0.22.1 + languageName: unknown + linkType: soft + "@discordjs/builders@workspace:^, @discordjs/builders@workspace:packages/builders": version: 0.0.0-use.local resolution: "@discordjs/builders@workspace:packages/builders" @@ -2649,6 +2671,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -3138,7 +3167,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor@npm:^7.32.0": +"@microsoft/api-extractor@npm:^7.32.0, @microsoft/api-extractor@npm:^7.32.1": version: 7.32.1 resolution: "@microsoft/api-extractor@npm:7.32.1" dependencies: @@ -3198,6 +3227,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:^2.8.0": + version: 2.8.0 + resolution: "@msgpack/msgpack@npm:2.8.0" + checksum: bead9393f57239007a2fe455df5277cbc03b125f14f310162a652b81471dcf3ab6780eaa24b36e20aa742998910a6840147d08b7267063b8e2de5d40c624360e + languageName: node + linkType: hard + "@next/env@npm:12.3.1": version: 12.3.1 resolution: "@next/env@npm:12.3.1" @@ -4293,6 +4329,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^16.11.52": + version: 16.11.65 + resolution: "@types/node@npm:16.11.65" + checksum: 81d84cb1e7aa305574cd35acf1a5e47f4a7f52783ba096f4bc511314540dee33a27cbd4fddc8bddd4535f1c87a96a76907157bcde093a25c89e8851d6dd63022 + languageName: node + linkType: hard + "@types/node@npm:^8.0.0": version: 8.10.66 resolution: "@types/node@npm:8.10.66" @@ -5069,6 +5112,16 @@ __metadata: languageName: node linkType: hard +"@vitest/coverage-c8@npm:^0.22.1": + version: 0.22.1 + resolution: "@vitest/coverage-c8@npm:0.22.1" + dependencies: + c8: ^7.12.0 + vitest: 0.22.1 + checksum: 141c10127a556ff32e43c6d92a468d800d7c62c767feef1a4123e204a07b58456b410efdba720fa3035639903098dc12767602aa7dabadd40e2d60abc0b008f1 + languageName: node + linkType: hard + "@vitest/coverage-c8@npm:^0.24.1": version: 0.24.1 resolution: "@vitest/coverage-c8@npm:0.24.1" @@ -5329,9 +5382,9 @@ __metadata: linkType: hard "ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": - version: 6.1.1 - resolution: "ansi-styles@npm:6.1.1" - checksum: f2b1ed658ead23caf77effe7b875960cacd70d1ebe47c830e191358b242d688cf52a28d55ef9b19d102f792e8c1dec34bd865db264f1c7f4f63dd3a5fa84677e + version: 6.2.0 + resolution: "ansi-styles@npm:6.2.0" + checksum: ce35204dc87f418440e8a95569c23e235715d7089e512f88254fb5fcedc18cdcfd6cd36852182388586eba21a9246b67a9ce4f1687864b06017407d8fda11a10 languageName: node linkType: hard @@ -6815,6 +6868,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.1 + resolution: "cluster-key-slot@npm:1.1.1" + checksum: 2fb7390e7950075acb09fc8aad3dc939abb64b139ba1b5f6341efdd0beda8cdc8b508e5f30d943370cf30ea0c13741c579e0846efd007b328bdc1a5a712264da + languageName: node + linkType: hard + "cmdk@npm:^0.1.20": version: 0.1.20 resolution: "cmdk@npm:0.1.20" @@ -7961,6 +8021,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.0.1": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -8347,9 +8414,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.4.251": - version: 1.4.276 - resolution: "electron-to-chromium@npm:1.4.276" - checksum: 9cd4448f68a37e598ad7ce193c982e8dd428c5e67e09cf780c0e13f23d689f7aec9b33718a61ea522b51ec2a8c835a0c25bbb0214ac62627fe7e4771a8650600 + version: 1.4.277 + resolution: "electron-to-chromium@npm:1.4.277" + checksum: 05002adf87dbaa6beb3895b7d64a1ee9442d46d5765582c71fc47c9aa8a4e710ada9939ce33bcd92da7c6fe61099462e036890b2edd21200c7f694309b31ea08 languageName: node linkType: hard @@ -11791,6 +11858,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.2.3": + version: 5.2.3 + resolution: "ioredis@npm:5.2.3" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.0.1 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 2cb7f0f4217e6774accad3620af1b7114722721c1d1824be2c9f0c2a77ab9629f2e0848d18b1a7208bc37796ae1207cb3e0898fce61900cfe797da0382724ad1 + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -13401,6 +13485,13 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 + languageName: node + linkType: hard + "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -13408,6 +13499,13 @@ __metadata: languageName: node linkType: hard +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 + languageName: node + linkType: hard + "lodash.isequal@npm:^4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" @@ -15128,8 +15226,8 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 9.2.0 - resolution: "node-gyp@npm:9.2.0" + version: 9.3.0 + resolution: "node-gyp@npm:9.3.0" dependencies: env-paths: ^2.2.0 glob: ^7.1.4 @@ -15143,7 +15241,7 @@ __metadata: which: ^2.0.2 bin: node-gyp: bin/node-gyp.js - checksum: 91f0589eabbd37f0d4e3fe9918f1f9e25afc707f6e107f0133be19c5aac62e731d92abdc2b106258665a4487b18cc2878d3fcd3dc2c6cffd68da1cb2a5ccf450 + checksum: 589ddd3ed967724ef425f9624bfa47cf73022640ab3eba6d556e92cdc4ddef33b63fce3a467c93b995a3f61df92eafd3c3d1e8dbe4a2c00c383334487dea99c3 languageName: node linkType: hard @@ -16813,6 +16911,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: ^1.0.0 + checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a + languageName: node + linkType: hard + "reduce-extract@npm:^1.0.0": version: 1.0.0 resolution: "reduce-extract@npm:1.0.0" @@ -18288,6 +18402,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -19035,6 +19156,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^0.2.4": + version: 0.2.4 + resolution: "tinypool@npm:0.2.4" + checksum: f050bd36c89529a2a0d3f9c1fdbba3f317114e3ee6eb5d5ba72c51e887d45ef3ef8d8533fb2ca2eba7189d19d2231712b81b3a75e099248532f5563369929c33 + languageName: node + linkType: hard + "tinypool@npm:^0.3.0": version: 0.3.0 resolution: "tinypool@npm:0.3.0" @@ -20487,7 +20615,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^3.0.0, vite@npm:^3.0.9, vite@npm:^3.1.7, vite@npm:~3.1.3": +"vite@npm:^2.9.12 || ^3.0.0-0, vite@npm:^3.0.0, vite@npm:^3.0.9, vite@npm:^3.1.7, vite@npm:~3.1.3": version: 3.1.7 resolution: "vite@npm:3.1.7" dependencies: @@ -20519,6 +20647,42 @@ __metadata: languageName: node linkType: hard +"vitest@npm:0.22.1, vitest@npm:^0.22.1": + version: 0.22.1 + resolution: "vitest@npm:0.22.1" + dependencies: + "@types/chai": ^4.3.3 + "@types/chai-subset": ^1.3.3 + "@types/node": "*" + chai: ^4.3.6 + debug: ^4.3.4 + local-pkg: ^0.4.2 + tinypool: ^0.2.4 + tinyspy: ^1.0.2 + vite: ^2.9.12 || ^3.0.0-0 + peerDependencies: + "@edge-runtime/vm": "*" + "@vitest/browser": "*" + "@vitest/ui": "*" + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 7abe50ceb51181e77cd62eb3a07c2da17f13078f09be34cc2e98f1f94a77eba33a56c644d48ae16bb474945ffc1cfc8664b1f4976c3de495c5e474057420c4ca + languageName: node + linkType: hard + "vitest@npm:0.24.1, vitest@npm:^0.24.1": version: 0.24.1 resolution: "vitest@npm:0.24.1"