commit e0c588ca1c8d569e22ba1584e15fa8169b70c301 Author: upstream-mirror Date: Tue Sep 15 17:21:35 2026 +0500 snapshot of https://github.com/ueberdosis/hocuspocus.git @ cf3cfa355b4d3bb61c6b6c80071f71783e32470e (2026-09-15) diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..cb1908b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,68 @@ +inheritance: true + +reviews: + request_changes_workflow: true + high_level_summary_instructions: + Short and simple, keep it cohesive, no complex wording, assume everyone including the + PR reviewer is new to the project or to code in general - simple english so even non-english native speakers + understand everything just fine. + high_level_summary_in_walkthrough: true + in_progress_fortune: false + slop_detection: + label: slop + suggested_labels: true + auto_apply_labels: true + labeling_instructions: + - label: 'complexity: easy' + instructions: Apply this label if the PR makes a clearly scoped change in one isolated component or package. + - label: 'complexity: expert' + instructions: Apply this label if the PR changes fundamental architecture, core APIs, or collaboration behavior. + - label: 'complexity: hard' + instructions: Apply this label if the PR changes Y.js document synchronization, awareness protocol, or WebSocket provider internals. + - label: 'complexity: medium' + instructions: Apply this label if the PR changes multiple components or the Y.js/Hocuspocus integration with a clear implementation path. + - label: 'complexity: trivial' + instructions: Apply this label if the PR is an obvious change under 10 lines, such as a documentation typo or missing type check. + - label: good first issue + instructions: Apply this label if the PR is a small, self-contained change that is easy for a first-time contributor to understand. + - label: 'impact: critical' + instructions: Apply this label if the PR fixes data loss, a security vulnerability, broken core functionality, or a problem affecting nearly all users of a feature. + - label: 'impact: high' + instructions: Apply this label if the PR fixes behavior affecting a large portion of users or a common workflow. + - label: 'impact: low' + instructions: Apply this label if the PR fixes an obscure edge case, a specific browser condition, or a rarely used feature. + - label: 'impact: medium' + instructions: Apply this label if the PR fixes a common feature usage or a workflow shared by a group of users. + auto_review: + ignore_usernames: + - github-actions[bot] + +issue_enrichment: + auto_enrich: + enabled: true + labeling: + auto_apply_labels: true + labeling_instructions: + - label: 'complexity: easy' + instructions: 'clearly described, affects one isolated component, fix localized to a single package' + - label: 'complexity: expert' + instructions: 'fundamental architecture conflict, core API changes affecting all extensions, or concurrent/collaboration behavior' + - label: 'complexity: hard' + instructions: 'requires deep understanding of Y.js document sync, awareness protocol, or WebSocket provider internals; no obvious solution' + - label: 'complexity: medium' + instructions: 'touches multiple components or requires understanding Y.js/Hocuspocus interaction, but solution path is clear' + - label: 'complexity: trivial' + instructions: 'typo in docs, missing type check, obvious fix in <10 lines' + - label: good first issue + instructions: + Apply this label if the issue is a good ticket to pick up as a first-time contributor or beginner at + Hocuspocus or Y.js. Good candidates are well-scoped bugs with clear reproduction steps, + documentation improvements, or small feature requests with a clear implementation path. + - label: 'impact: critical' + instructions: 'data loss, security vulnerability, completely broken core functionality, or affects nearly everyone using a specific feature' + - label: 'impact: high' + instructions: 'affects a large portion of users, blocks a common use case, or causes noticeable UX problem many will run into' + - label: 'impact: low' + instructions: 'obscure edge case, specific browser under specific conditions, or feature request very few would use' + - label: 'impact: medium' + instructions: 'affects a common usage of a feature (e.g. specific extension combination) or handful of users with similar workflows' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..efb4918 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +insert_final_newline = true +max_line_length = 100 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b4a4e49 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +**/dist/** +**/node_modules/** \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5c83390 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve Hocuspocus +url: https://github.com/ueberdosis/hocuspocus/issues/new +title: '' +labels: + - bug +assignees: + - janthurau +--- + +**Description** +A clear and concise description of what the bug is. + +**Steps to reproduce the bug** +Steps to reproduce the behavior: +1. Go to … +2. Type in … +3. Click on … +4. See error message + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshot, video, or GIF** +If applicable, add screenshots to help explain your problem. + +**Environment?** + - operating system: + - browser: + - mobile/desktop: + - Hocuspocus version: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e83753e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Discuss hocuspocus on GitHub + url: https://github.com/ueberdosis/hocuspocus/discussions/new + about: Help, discussion about best practices, or any other conversation that would benefit from being searchable + - name: Join the hocuspocus Discord server + url: https://discord.gg/WtJ49jGshW + about: Ccasual chit-chat with others using hocuspocus diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4e5849f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest a new feature for Hocuspocus +title: '' +labels: + - feature request +assignees: + - janthurau +--- + +**The problem I am facing** +A clear and concise description of what the problem is. For example: I’m always frustrated when … + +**The solution I would like** +A clear and concise description of what you want to happen. + +**Alternatives I have considered** +A clear and concise description of any alternative solutions or features you have considered. + +**Additional context** +Add any other context or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/feedback.md b/.github/ISSUE_TEMPLATE/feedback.md new file mode 100644 index 0000000..4a11d9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feedback.md @@ -0,0 +1,22 @@ +--- +name: Feedback on the documentation +about: Share what we need to explain better +url: https://github.com/ueberdosis/hocuspocus/issues/new +title: '' +labels: + - documentation +assignees: + - janthurau +--- + +**Part of the documentation?** +I’ve read the following page of the documentation … + +**Really helpful parts** +I think this part is really good: … + +**Hard to understand, missing or misleading** +But you really need to improve … + +**Additional context** +Add any other context or screenshots here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0986a47 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,78 @@ +# Dependabot creates pull requests to keep your dependencies secure and up-to-date. +# Documentation: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: 'github-actions' + directory: '/' + open-pull-requests-limit: 10 + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/cli" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/common" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-database" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-logger" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-redis" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-sqlite" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-throttle" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/extension-webhook" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/provider" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/server" + schedule: + interval: 'weekly' + day: 'monday' + + - package-ecosystem: "npm" + directory: "packages/transformer" + schedule: + interval: 'weekly' + day: 'monday' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2bedf88 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,109 @@ +# Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. +# Documentation: https://docs.github.com/en/actions + +name: build + +on: + push: + branches: + - main + pull_request: + branches: + - '**' + +jobs: + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22, 23] + + steps: + + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6.0.10 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Set up npmrc + run: echo "@tiptap-cloud:registry=https://registry.tiptap.dev/" >> ~/.npmrc && echo "//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_PRIVATE_REGISTRY_NPM_TOKEN }}" >> ~/.npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + +# - name: Lint code +# run: pnpm lint + + test: + runs-on: ubuntu-latest + + services: + redis: + image: redis + ports: + - 6379/tcp + options: --entrypoint redis-server + + strategy: + matrix: + node-version: [22, 23] + + steps: + + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6.0.10 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Set up npmrc + run: echo "@tiptap-cloud:registry=https://registry.tiptap.dev/" >> ~/.npmrc && echo "//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_PRIVATE_REGISTRY_NPM_TOKEN }}" >> ~/.npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + env: + REDIS_HOST: localhost + REDIS_PORT: ${{ job.services.redis.ports[6379] }} + + build: + runs-on: ubuntu-latest + + needs: lint + + strategy: + matrix: + node-version: [22, 23] + + steps: + + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6.0.10 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Set up npmrc + run: echo "@tiptap-cloud:registry=https://registry.tiptap.dev/" >> ~/.npmrc && echo "//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_PRIVATE_REGISTRY_NPM_TOKEN }}" >> ~/.npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Try to build the packages + run: pnpm build:packages diff --git a/.github/workflows/docsearch.yml b/.github/workflows/docsearch.yml new file mode 100644 index 0000000..5e4f78b --- /dev/null +++ b/.github/workflows/docsearch.yml @@ -0,0 +1,27 @@ +# Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. +# Documentation: https://docs.github.com/en/actions + +name: docsearch + +on: + workflow_dispatch: + schedule: + - cron: '5 1 * * *' + +jobs: + + docsearch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Run DocSearch Scraper + shell: bash + run: | + docker run \ + -e TYPESENSE_API_KEY=${{ secrets.TYPESENSE_API_KEY }} \ + -e TYPESENSE_HOST="${{ secrets.TYPESENSE_HOST }}" \ + -e TYPESENSE_PORT="${{ secrets.TYPESENSE_PORT }}" \ + -e TYPESENSE_PROTOCOL="${{ secrets.TYPESENSE_PROTOCOL }}" \ + -e CONFIG="$(cat docsearch.config.json | jq -r tostring)" \ + typesense/docsearch-scraper diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..8a144c6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,39 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages + +name: Publish to NPM + +on: + push: + tags: + - '*' + +permissions: + id-token: write + contents: read + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6.0.10 + + - uses: actions/setup-node@v7 + with: + node-version: 22 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + + - name: Set up npmrc + id: setup-npmrc + run: echo "@tiptap-cloud:registry=https://registry.tiptap.dev/" >> ~/.npmrc && echo "//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_PRIVATE_REGISTRY_NPM_TOKEN }}" >> ~/.npmrc + + - run: pnpm install --frozen-lockfile + + - run: pnpm publish:latest + if: "!contains(github.ref, '-rc.')" + + - run: pnpm publish:pre + if: "contains(github.ref, '-rc.')" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a976da --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.log +.cache +.DS_Store +.env +.env.* +.temp +dist +node_modules +.idea + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/database +.prettierignore +package-lock.json +yarn.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..91c8696 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.defaultFormatter": "dbaeumer.vscode-eslint" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6a3c5f7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1432 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + + +### Bug Fixes + +* emit maxAttemptsFailed instead of unhandled rejection ([#1149](https://github.com/ueberdosis/hocuspocus/issues/1149)) ([0218bc0](https://github.com/ueberdosis/hocuspocus/commit/0218bc04aebd050946da2542e8f648ccf4f93d30)) +* **extension-redis:** publish sync replies to the requester only ([#1152](https://github.com/ueberdosis/hocuspocus/issues/1152)) ([7fb9086](https://github.com/ueberdosis/hocuspocus/commit/7fb9086597a12a9220826330eb83426e081f7eea)), closes [#1151](https://github.com/ueberdosis/hocuspocus/issues/1151) + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + + +### Bug Fixes + +* encode stateless message once when received operation via Redis ; this is a performance fix. ([#1136](https://github.com/ueberdosis/hocuspocus/issues/1136)) ([b524b4b](https://github.com/ueberdosis/hocuspocus/commit/b524b4b30299a64ffa1309f70a0fd6e761103d4a)) + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* audit ([141360c](https://github.com/ueberdosis/hocuspocus/commit/141360c256022deb5578c3902c3dfe0af8f6516e)) +* flawky test relying on timings ([fe4a8e6](https://github.com/ueberdosis/hocuspocus/commit/fe4a8e68801f1659624f53da745e595ad9f11c63)) +* ignore message in awarenessUpdateHandler if origin=this ([#1129](https://github.com/ueberdosis/hocuspocus/issues/1129)) ([08b25d4](https://github.com/ueberdosis/hocuspocus/commit/08b25d4b258d932c68c999c14edcb4efc65c7a9b)) +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) +* when beforeHandleMessage throws, we don't want to process other messages that were already queued ([#1123](https://github.com/ueberdosis/hocuspocus/issues/1123)) ([ed5dc40](https://github.com/ueberdosis/hocuspocus/commit/ed5dc40581cc829a6d0b04040717a8ee89296140)) + + +### Features + +* pnpm11 ([#1133](https://github.com/ueberdosis/hocuspocus/issues/1133)) ([01c224a](https://github.com/ueberdosis/hocuspocus/commit/01c224ad9133340048c0e4f7bdce3981f4984d76)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + + +### Bug Fixes + +* allow binding the server to a specific address ([#1121](https://github.com/ueberdosis/hocuspocus/issues/1121)) ([408127b](https://github.com/ueberdosis/hocuspocus/commit/408127b1c090356cc9148a801f314a8e6f863b09)) + + +### Features + +* add `flushDelay` option for batching updates to reduce websocket traffic during heavy editing ([#1118](https://github.com/ueberdosis/hocuspocus/issues/1118)) ([75594c0](https://github.com/ueberdosis/hocuspocus/commit/75594c05d57d48f2f70d4c9440c28b8226bf95ac)) +* add consistent state synchronization across Redis instances ([#1119](https://github.com/ueberdosis/hocuspocus/issues/1119)) ([0051a6c](https://github.com/ueberdosis/hocuspocus/commit/0051a6cb7618290d1f574da7ad61da2be77f839d)) + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + + +### Features + +* add `afterHandleMessage` hook to run after message handling completion ([#1112](https://github.com/ueberdosis/hocuspocus/issues/1112)) ([abec66d](https://github.com/ueberdosis/hocuspocus/commit/abec66db76ab43737e3e5baf8a461ea38637d5d6)) +* enforce pre-auth resource limits to safeguard server stability ([#1113](https://github.com/ueberdosis/hocuspocus/issues/1113)) ([6e1a932](https://github.com/ueberdosis/hocuspocus/commit/6e1a932662f38074b732b5e51394461b91809570)) + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + + +### Features + +* add `unloadImmediately` option to `disconnect()` for configurable document persistence behavior ([#1111](https://github.com/ueberdosis/hocuspocus/issues/1111)) ([eeb4696](https://github.com/ueberdosis/hocuspocus/commit/eeb469630540b8aeb08d699ba130a86e50e7ee5e)) + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + + +### Bug Fixes + +* delete debouncer timer entry before awaiting the in-flight execution ([#1110](https://github.com/ueberdosis/hocuspocus/issues/1110)) ([f05f1d0](https://github.com/ueberdosis/hocuspocus/commit/f05f1d02f7890822e76d7b16f3cf3b3d9901ab02)), closes [#1014](https://github.com/ueberdosis/hocuspocus/issues/1014) + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + + +### Bug Fixes + +* client-initiated broadcastStateless ([#1103](https://github.com/ueberdosis/hocuspocus/issues/1103)) ([6872d08](https://github.com/ueberdosis/hocuspocus/commit/6872d08bbb2ebe8175dcfb51953481b2f4988291)) + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + + +### Bug Fixes + +* **server:** wrap awareness origin as TransactionOrigin ([#1094](https://github.com/ueberdosis/hocuspocus/issues/1094)) ([bb24de0](https://github.com/ueberdosis/hocuspocus/commit/bb24de0ffb944e10845174e26971eaa95b2db675)) + + +### Features + +* **server:** add beforeHandleAwareness hook ([#1096](https://github.com/ueberdosis/hocuspocus/issues/1096)) ([1597da8](https://github.com/ueberdosis/hocuspocus/commit/1597da8217c4ed20b06d46052f1db7c7765d6153)) + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package hocuspocus + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + + +### Bug Fixes + +* provider-react ESM now emits a clean import { jsx } from "react/jsx-runtime" and zero CJS shims ([e40afd9](https://github.com/ueberdosis/hocuspocus/commit/e40afd94935eff7e3c607a379511cc83c67d72ac)) + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + + +### Bug Fixes + +* react provider in strict mode ([4f27b7f](https://github.com/ueberdosis/hocuspocus/commit/4f27b7f0e57423b08fabb59cfa6d0a2cc67bc996)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + + +### Bug Fixes + +* publishConfig for provider-react ([376765f](https://github.com/ueberdosis/hocuspocus/commit/376765f6623259c79940da67a1f970ac9a1e84b1)) + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Bug Fixes + +* remove unused packages ([#1090](https://github.com/ueberdosis/hocuspocus/issues/1090)) ([66fb35c](https://github.com/ueberdosis/hocuspocus/commit/66fb35cff71d986d671fe9a8118ce4da87507b44)) + + +### Features + +* first draft of @hocuspocus/provider-react package ([#1059](https://github.com/ueberdosis/hocuspocus/issues/1059)) ([cbc9977](https://github.com/ueberdosis/hocuspocus/commit/cbc99770096e8c674dae304c7e1a16e0fbec827b)) +* remove axios ([#1089](https://github.com/ueberdosis/hocuspocus/issues/1089)) ([fe99d02](https://github.com/ueberdosis/hocuspocus/commit/fe99d02496cae65f9c3c4f1b37c0c6823f05f95a)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + + +### Bug Fixes + +* turn off sessionAwareness by default to keep BC with old servers ([3a57071](https://github.com/ueberdosis/hocuspocus/commit/3a57071517adb5c9e037fb7c0ac169ffc330df90)) + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + + +### Bug Fixes + +* reject whitespaces in documents ([3c19888](https://github.com/ueberdosis/hocuspocus/commit/3c198883b50cddf02141debbb74d5250948ee310)) +* when storeDocumentHooks throw, retry until storing suceeds. ([99db64d](https://github.com/ueberdosis/hocuspocus/commit/99db64df234946f2492129e793298734c5e2c503)) + + +### Features + +* adds deduplication for queued awareness messages ([e0453f0](https://github.com/ueberdosis/hocuspocus/commit/e0453f0210a4a30f6f8b742629ed777724429df9)) +* optimize retry logic ([302eb08](https://github.com/ueberdosis/hocuspocus/commit/302eb08d9a2a2b59d44424571fdc70d5790d8fd8)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Bug Fixes + +* adds @aws-sdk/client-s3 to devDependencies to fix possible test issues with pnpm ([d36773c](https://github.com/ueberdosis/hocuspocus/commit/d36773cc62d385c32e16e46b4b9a18079eed3e0e)) +* adds override for vulnerable packages (mostly xml DDoS) ([de075ff](https://github.com/ueberdosis/hocuspocus/commit/de075ffd9a6958782d2ae8ea64286be2168b71dd)) +* check code is type number ([#1062](https://github.com/ueberdosis/hocuspocus/issues/1062)) ([6dfa610](https://github.com/ueberdosis/hocuspocus/commit/6dfa610b35fa1aab7a7f0dd2f2a216cc3094612f)) +* force-run debounced ops on server shutdown (even when unloadImmediately=false) ([dba9984](https://github.com/ueberdosis/hocuspocus/commit/dba99845a52e798884cda7e2c81d16e4dc462e9c)) +* npm audit ([ba727ea](https://github.com/ueberdosis/hocuspocus/commit/ba727ea8a2a60c961d683116393291b43be3051d)) +* onLoadDocument now accepts a yjs update (Uint8Array or Buffer) or a Y.Doc. fixes [#795](https://github.com/ueberdosis/hocuspocus/issues/795), [#271](https://github.com/ueberdosis/hocuspocus/issues/271) ([667e145](https://github.com/ueberdosis/hocuspocus/commit/667e14535d2da3986ecc731b18c976b3662552aa)) +* optimize memory usage by creating message uint8arrays once, not per connection ([#1051](https://github.com/ueberdosis/hocuspocus/issues/1051)) ([ad398a8](https://github.com/ueberdosis/hocuspocus/commit/ad398a8304908068df8e11c87edf9962f416751a)) +* reset auth state on failed auth, so a new auth message can run auth hooks again. fixes [#944](https://github.com/ueberdosis/hocuspocus/issues/944) ([#1065](https://github.com/ueberdosis/hocuspocus/issues/1065)) ([81ce838](https://github.com/ueberdosis/hocuspocus/commit/81ce8382a80509bad7d8c6581ff87e9d91b26179)) +* trigger storeDocumentHooks on any change to the doc (except if incoming via redis). Before, it was easy to accidentally prevent changes from being saved by forgetting to include an origin in the Yjs update ([81975e7](https://github.com/ueberdosis/hocuspocus/commit/81975e7a38b4b594024863278e61ce5ecf318121)) +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + +### Features + +* add generic `Context` support across server classes and hooks ([#1057](https://github.com/ueberdosis/hocuspocus/issues/1057)) ([67741a0](https://github.com/ueberdosis/hocuspocus/commit/67741a0661004f1d222795bb99e68d27a5a701aa)) +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* migrate from sqlite3 to better-sqlite3, as sqlite3 is now unmaintained. ([a247146](https://github.com/ueberdosis/hocuspocus/commit/a24714624c6effdb521e84352f0623d24be5d3f0)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* pass context when using DirectConnection. Do not run storeDocumentHooks on any transact, just on disconnect ([ffa32b7](https://github.com/ueberdosis/hocuspocus/commit/ffa32b7a96a15d953b031df1005df6cfe7624d51)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) +* upgrade ava ([#1075](https://github.com/ueberdosis/hocuspocus/issues/1075)) ([9a23823](https://github.com/ueberdosis/hocuspocus/commit/9a23823c5100b2e87d23a5cbeeafde45b847a970)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package hocuspocus + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package hocuspocus + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package hocuspocus + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package hocuspocus + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + + +### Bug Fixes + +* adds @aws-sdk/client-s3 to devDependencies to fix possible test issues with pnpm ([d36773c](https://github.com/ueberdosis/hocuspocus/commit/d36773cc62d385c32e16e46b4b9a18079eed3e0e)) +* adds override for vulnerable packages (mostly xml DDoS) ([de075ff](https://github.com/ueberdosis/hocuspocus/commit/de075ffd9a6958782d2ae8ea64286be2168b71dd)) +* force-run debounced ops on server shutdown (even when unloadImmediately=false) ([dba9984](https://github.com/ueberdosis/hocuspocus/commit/dba99845a52e798884cda7e2c81d16e4dc462e9c)) +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + +### Features + +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) +* upgrade ava ([#1075](https://github.com/ueberdosis/hocuspocus/issues/1075)) ([9a23823](https://github.com/ueberdosis/hocuspocus/commit/9a23823c5100b2e87d23a5cbeeafde45b847a970)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Bug Fixes + +* check code is type number ([#1062](https://github.com/ueberdosis/hocuspocus/issues/1062)) ([6dfa610](https://github.com/ueberdosis/hocuspocus/commit/6dfa610b35fa1aab7a7f0dd2f2a216cc3094612f)) +* npm audit ([ba727ea](https://github.com/ueberdosis/hocuspocus/commit/ba727ea8a2a60c961d683116393291b43be3051d)) +* onLoadDocument now accepts a yjs update (Uint8Array or Buffer) or a Y.Doc. fixes [#795](https://github.com/ueberdosis/hocuspocus/issues/795), [#271](https://github.com/ueberdosis/hocuspocus/issues/271) ([667e145](https://github.com/ueberdosis/hocuspocus/commit/667e14535d2da3986ecc731b18c976b3662552aa)) +* optimize memory usage by creating message uint8arrays once, not per connection ([#1051](https://github.com/ueberdosis/hocuspocus/issues/1051)) ([ad398a8](https://github.com/ueberdosis/hocuspocus/commit/ad398a8304908068df8e11c87edf9962f416751a)) +* reset auth state on failed auth, so a new auth message can run auth hooks again. fixes [#944](https://github.com/ueberdosis/hocuspocus/issues/944) ([#1065](https://github.com/ueberdosis/hocuspocus/issues/1065)) ([81ce838](https://github.com/ueberdosis/hocuspocus/commit/81ce8382a80509bad7d8c6581ff87e9d91b26179)) +* trigger storeDocumentHooks on any change to the doc (except if incoming via redis). Before, it was easy to accidentally prevent changes from being saved by forgetting to include an origin in the Yjs update ([81975e7](https://github.com/ueberdosis/hocuspocus/commit/81975e7a38b4b594024863278e61ce5ecf318121)) + + +### Features + +* add generic `Context` support across server classes and hooks ([#1057](https://github.com/ueberdosis/hocuspocus/issues/1057)) ([67741a0](https://github.com/ueberdosis/hocuspocus/commit/67741a0661004f1d222795bb99e68d27a5a701aa)) +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate from sqlite3 to better-sqlite3, as sqlite3 is now unmaintained. ([a247146](https://github.com/ueberdosis/hocuspocus/commit/a24714624c6effdb521e84352f0623d24be5d3f0)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* pass context when using DirectConnection. Do not run storeDocumentHooks on any transact, just on disconnect ([ffa32b7](https://github.com/ueberdosis/hocuspocus/commit/ffa32b7a96a15d953b031df1005df6cfe7624d51)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + + +### Bug Fixes + +* dev setup (use local packages instead of the released ones) ([0137da0](https://github.com/ueberdosis/hocuspocus/commit/0137da0a32d45002f37ed933f886e0fc737997bc)) +* dont release demos ([148e12f](https://github.com/ueberdosis/hocuspocus/commit/148e12f6fdbd4982076b726afdc8d6c77edfb9a0)) + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + + +### Bug Fixes + +* dont include all types in all packages ([4dade25](https://github.com/ueberdosis/hocuspocus/commit/4dade250371c641a23c8df1fd2814d56c00d8b59)) + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + + +### Bug Fixes + +* fixes [#1041](https://github.com/ueberdosis/hocuspocus/issues/1041): websocket URL ending with / wont work ([#1046](https://github.com/ueberdosis/hocuspocus/issues/1046)) ([0df9b09](https://github.com/ueberdosis/hocuspocus/commit/0df9b095d47e7953a9dff090942e937894d8f7fc)) +* fixes hono types in playground ([#1045](https://github.com/ueberdosis/hocuspocus/issues/1045)) ([163fbed](https://github.com/ueberdosis/hocuspocus/commit/163fbed167222eac8e89095cca09074bb7ddf6e2)) +* fixes provider.forwardClose type error ([5438500](https://github.com/ueberdosis/hocuspocus/commit/543850064ba1cb18c13cb4211cec76a3ad05ddb4)) +* fixes typings for onTokenSync ([b2ac132](https://github.com/ueberdosis/hocuspocus/commit/b2ac13250b2ddc6bb41ce456b56bac8db2a6e0d5)) +* strict types for authorizedScope ([#1029](https://github.com/ueberdosis/hocuspocus/issues/1029)) ([44e1b5d](https://github.com/ueberdosis/hocuspocus/commit/44e1b5d19f632bf646b654efd2c2ba70587adf62)) + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + + +### Bug Fixes + +* fixes memory leak under high load, replace nextTick by setTimeout ([1f0b5a4](https://github.com/ueberdosis/hocuspocus/commit/1f0b5a4f9084c0b6e01ddd19ec831d2e2664fc23)) + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + + +### Bug Fixes + +* on awareness craah prevention and memory leak onStoreDocument ([#1032](https://github.com/ueberdosis/hocuspocus/issues/1032)) ([67957ae](https://github.com/ueberdosis/hocuspocus/commit/67957ae4d114c07b8b63cf2900a7ae07f1300f72)), closes [#1](https://github.com/ueberdosis/hocuspocus/issues/1) [#3](https://github.com/ueberdosis/hocuspocus/issues/3) [#1027](https://github.com/ueberdosis/hocuspocus/issues/1027) + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + + +### Bug Fixes + +* fixes timing issues with debouncer: new debounced functions may start before the previous had finished ([708bfdb](https://github.com/ueberdosis/hocuspocus/commit/708bfdb0197833d5d6371b4f2c930fe815a66d15)) +* make beforeSync not async, as this may break message processing order (see https://github.com/yjs/yjs/issues/591) ([abeb298](https://github.com/ueberdosis/hocuspocus/commit/abeb2985d522539d30ea96217417a300569f77fd)) +* make beforeSync not async, as this may break message processing order (see issue 591 of yjs) ([1d11bea](https://github.com/ueberdosis/hocuspocus/commit/1d11bea8f78572a4138442ee063b6fa02890248d)) +* make handleUpdate sync to avoid timing issues ([9a6485b](https://github.com/ueberdosis/hocuspocus/commit/9a6485b9c0ee2f193e9ba68b39a8c93196a796bf)) + + +### Features + +* adds Document.lastChangeTime, which is set to Date.now() whenever a change on the documment has been detected ([9092efd](https://github.com/ueberdosis/hocuspocus/commit/9092efd604f20e1ec5228a93dd8b1ee9b48d5e98)) + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + + +### Reverts + +* Revert "fix: temporarily removes beforeSync callback (#919, partially reverts a6a7bcd0768378908ffb5d32096183280115631b). Making applySync async causes timing issues that leads to issues when using PermanentUserData. This can be reproduced by using the playground server with sqlite and a sleep(1000). Additionally, use Y.PermanentUserData, or try to read data from the ydoc in the onSynced event." ([e33dbe2](https://github.com/ueberdosis/hocuspocus/commit/e33dbe22d6ec98279699e11ef6d5bb224299e2d3)), closes [#919](https://github.com/ueberdosis/hocuspocus/issues/919) + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + + +### Bug Fixes + +* temporarily removes beforeSync callback ([#919](https://github.com/ueberdosis/hocuspocus/issues/919), partially reverts a6a7bcd0768378908ffb5d32096183280115631b). Making applySync async causes timing issues that leads to issues when using PermanentUserData. This can be reproduced by using the playground server with sqlite and a sleep(1000). Additionally, use Y.PermanentUserData, or try to read data from the ydoc in the onSynced event. ([7192664](https://github.com/ueberdosis/hocuspocus/commit/719266443e13e0dab3f69cd8e10b29da547ceab1)) + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + + +### Bug Fixes + +* fixes timing issues when multiple users leave the same document together ([70e5955](https://github.com/ueberdosis/hocuspocus/commit/70e59553e95acbeaf170e0e5a13ea14cfd4ff152)) + + +### Features + +* add onTokenSync hook for auth token re-sync ([#1001](https://github.com/ueberdosis/hocuspocus/issues/1001)) ([ee252fb](https://github.com/ueberdosis/hocuspocus/commit/ee252fb99ea6faf80cc71d2e1ce51726e698b76b)) + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + + +### Bug Fixes + +* bind onUpdate before afterLoadDocument, as changes wont be saved otherwise ([bf8a6f9](https://github.com/ueberdosis/hocuspocus/commit/bf8a6f9b2902a81664bbbba8e8a34f7112570d2a)) + + +### Features + +* rewrite of redis unloading logic to utilize before/after-unloadDocument ([#1007](https://github.com/ueberdosis/hocuspocus/issues/1007)) ([8f86caf](https://github.com/ueberdosis/hocuspocus/commit/8f86caff4e74601ecc98fff293fc58584af27176)) + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Bug Fixes + +* delay adding document to instance until connection is about to be established ([#972](https://github.com/ueberdosis/hocuspocus/issues/972)) ([1e2917a](https://github.com/ueberdosis/hocuspocus/commit/1e2917aacb342c035645bb3c01af4910d699eaad)) +* emit status event when status is updated to connected in onOpen, fixes [#1004](https://github.com/ueberdosis/hocuspocus/issues/1004) ([14829c1](https://github.com/ueberdosis/hocuspocus/commit/14829c1ee429359b3648c3021942fd581aa52f4d)) +* **extension-redis:** avoid hard crashes of expected errors when acquiring a lock ([#983](https://github.com/ueberdosis/hocuspocus/issues/983)) ([38e58a4](https://github.com/ueberdosis/hocuspocus/commit/38e58a4d099cf0f5fba0f51cbd7be169de94b0bd)) +* set max listeners to infinity to avoid warnings when using multiplexing ([21412aa](https://github.com/ueberdosis/hocuspocus/commit/21412aa3f4bc861fa4ece46c2e57d5a0a48be8af)) + + +### Features + +* remove uuid dependency and replace usages by crypto.randomUUID ([#1005](https://github.com/ueberdosis/hocuspocus/issues/1005)) ([393189f](https://github.com/ueberdosis/hocuspocus/commit/393189fe448c68cbd741642866be4b4b23546f17)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + + +### Bug Fixes + +* fixes playground plug and play confg ([f9dff84](https://github.com/ueberdosis/hocuspocus/commit/f9dff843e5132fd858ce410e1774089e6b4c9692)) +* unload document in catch block of storeDocumentHooks ([#955](https://github.com/ueberdosis/hocuspocus/issues/955)) ([1a3b5b8](https://github.com/ueberdosis/hocuspocus/commit/1a3b5b8b2057e11ea16068f17efadd5c93437276)) + + +### Features + +* adds configuration option autoConnect (default=true) which allows creating the provider without triggering a connection attempt (fixes [#938](https://github.com/ueberdosis/hocuspocus/issues/938)) ([adc315f](https://github.com/ueberdosis/hocuspocus/commit/adc315f0633f6fdad310384b5217fbe25e29108a)) +* adds more error logging when a connection is closed due to an exception ([1c4090c](https://github.com/ueberdosis/hocuspocus/commit/1c4090cc464d724e74143296b6e05bde87f5f5bf)) + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + + +### Bug Fixes + +* **extension-redis:** redlock release race condition ([#958](https://github.com/ueberdosis/hocuspocus/issues/958)) ([5666a65](https://github.com/ueberdosis/hocuspocus/commit/5666a656f9bd2419fb8565c99a812698d1ff9dcf)) + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + + +### Bug Fixes + +* add catch on message receiver when handling message ([#967](https://github.com/ueberdosis/hocuspocus/issues/967)) ([f9ceb26](https://github.com/ueberdosis/hocuspocus/commit/f9ceb2626674d173037be8f8bf25179719ddf977)) +* correct database link in redis.md ([#968](https://github.com/ueberdosis/hocuspocus/issues/968)) ([ec47b50](https://github.com/ueberdosis/hocuspocus/commit/ec47b505e8b5367ff59f7faa8588102ac4e4e58a)) +* **extension-redis:** unsubscribe from pubsub ([#945](https://github.com/ueberdosis/hocuspocus/issues/945)) ([0812d48](https://github.com/ueberdosis/hocuspocus/commit/0812d4810c80ae6118c9376cced1d03c8f1f95dc)) +* race condition when multiple providers connect to the same socket ; fixes [#964](https://github.com/ueberdosis/hocuspocus/issues/964) ([#966](https://github.com/ueberdosis/hocuspocus/issues/966)) ([4868bae](https://github.com/ueberdosis/hocuspocus/commit/4868bae54f7a82c5b0601edc325d42fd1f9420fd)) + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + + +### Features + +* adds hono example to backend playground ([bb2d390](https://github.com/ueberdosis/hocuspocus/commit/bb2d3902fc406e2964f422eda52b4fcc035f734e)) +* playground ([ea54455](https://github.com/ueberdosis/hocuspocus/commit/ea544550b4b76f519a547008e3b77b76388996f2)) + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + + +### Features + +* HocuspocusProviderWebsocket: adds handleTimeout callback that is called when the connection doesnt establish within the given timeout ([509e31b](https://github.com/ueberdosis/hocuspocus/commit/509e31bdcc23b625f8245c8c2d2ea320729dd8e7)) + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + + +### Features + +* adds `document` to `beforeUnloadDocumentPayload` ([f7d1e64](https://github.com/ueberdosis/hocuspocus/commit/f7d1e646342746b9228fe1a8df8a80ee35e2aea5)) +* beforeUnloadDocument: allow passing in Hocuspocus constructor ; allow preventing unload by throwing an error in beforeUnloadDocument ([63b298e](https://github.com/ueberdosis/hocuspocus/commit/63b298eddf23b8f472a066dc8274b22d790176ea)) + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + + +### Bug Fixes + +* playground: Cannot update a component (`CollaborationStatus`) while rendering a different component (`CollaborativeEditor`) ([9c6b87d](https://github.com/ueberdosis/hocuspocus/commit/9c6b87d882d2a1c00dca55cac1d71de2221a3a6d)) +* playground: destroy instead of detach ([c8b60f8](https://github.com/ueberdosis/hocuspocus/commit/c8b60f84ea7b7df0d04a9aad71e92e8bba2c747c)) +* playground: port 1234 ([62e45d4](https://github.com/ueberdosis/hocuspocus/commit/62e45d40fd7e044daa6b0eee0b96b0786d2c5a6b)) +* playground: update styling of collaboration caret ([4a2506b](https://github.com/ueberdosis/hocuspocus/commit/4a2506ba0fe399280b7cd6cba31602c0660e7a36)) + + +### Features + +* playground: adds two editors for nicer testing ([b2332f2](https://github.com/ueberdosis/hocuspocus/commit/b2332f21c5369a6e980e19c1ef7842e8cf32b878)) + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + + +### Bug Fixes + +* fixes koa example in playground ([1a73d14](https://github.com/ueberdosis/hocuspocus/commit/1a73d14221a006f38d07063d7001ae8f1e14deaf)) + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + + +### Bug Fixes + +* fixes express/koa backend demo, fixes [#939](https://github.com/ueberdosis/hocuspocus/issues/939) ([ddec715](https://github.com/ueberdosis/hocuspocus/commit/ddec7153e7d40725d6551d7dfbc61aac1b2b042e)) + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package hocuspocus + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package hocuspocus + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + + +### Features + +* add beforeSync hook ([#919](https://github.com/ueberdosis/hocuspocus/issues/919)) ([a6a7bcd](https://github.com/ueberdosis/hocuspocus/commit/a6a7bcd0768378908ffb5d32096183280115631b)) + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package hocuspocus + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + + +### Bug Fixes + +* **comments:** fix error where deletedComments yArray does not exist ([#884](https://github.com/ueberdosis/hocuspocus/issues/884)) ([1e405ef](https://github.com/ueberdosis/hocuspocus/commit/1e405ef6da274c75c035131f7a2bf3eebad0fd4d)) +* pass `WebSocket` instead of Connection to `applyAwarenessUpdate` ([#882](https://github.com/ueberdosis/hocuspocus/issues/882)) ([e359652](https://github.com/ueberdosis/hocuspocus/commit/e3596525072261a04f7c9e62888c06ea33f886dd)) + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + + +### Bug Fixes + +* **provider:** allow deletion of first item ([3fe0b91](https://github.com/ueberdosis/hocuspocus/commit/3fe0b91cd5e13b53c6b2ffa9fdf6af3962c26b0b)) +* **provider:** correct event handler binding for document updates and awareness ([e3c616c](https://github.com/ueberdosis/hocuspocus/commit/e3c616c2cd18a717cd3c36a093f4954a7cac39fc)) +* **provider:** fix comment sorting ([e706537](https://github.com/ueberdosis/hocuspocus/commit/e7065378846260ed4ec59c3528a509b8239162c3)) + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + + +### Bug Fixes + +* improve global window checks to enable react native support ([#823](https://github.com/ueberdosis/hocuspocus/issues/823)) ([5dd53d5](https://github.com/ueberdosis/hocuspocus/commit/5dd53d554f5e54984a0743d25dfd16224a95a0d0)) + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + + +### Reverts + +* Revert "websocket: enable following redirects (#806)" (#807) ([930a61c](https://github.com/ueberdosis/hocuspocus/commit/930a61c81e111cda2b692bf1d89b38bfce5d721d)), closes [#806](https://github.com/ueberdosis/hocuspocus/issues/806) [#807](https://github.com/ueberdosis/hocuspocus/issues/807) + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + + +### Bug Fixes + +* allow resolvedAt to be null on updateThread ([eac0bf9](https://github.com/ueberdosis/hocuspocus/commit/eac0bf994d37b072549fe446557efa0c4b2f1568)) + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + + +### Bug Fixes + +* add `documentName` into broadcast message ([#713](https://github.com/ueberdosis/hocuspocus/issues/713)) ([8400928](https://github.com/ueberdosis/hocuspocus/commit/840092861fa2801c62d5c12d5574bfe24e266c3f)) + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + + +### Bug Fixes + +* check if ymap is empty ([#666](https://github.com/ueberdosis/hocuspocus/issues/666)) ([4450372](https://github.com/ueberdosis/hocuspocus/commit/4450372e7bff40693d32aa6b6c425dcfdb868ded)) + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + + +### Bug Fixes + +* hocuspocus provider auto connect and disconnect behavior ([#663](https://github.com/ueberdosis/hocuspocus/issues/663)) ([a035c09](https://github.com/ueberdosis/hocuspocus/commit/a035c09b0da4347b0de7cdbf4cfe7f59a14b76f3)) +* remove awareness states on page unload instead of beforeunload ([#658](https://github.com/ueberdosis/hocuspocus/issues/658)) ([e1bd3f1](https://github.com/ueberdosis/hocuspocus/commit/e1bd3f1c9d9486382e1a856faac5118220a39087)) + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + + +### Bug Fixes + +* docs/contributing.md ([#621](https://github.com/ueberdosis/hocuspocus/issues/621)) ([8dbe872](https://github.com/ueberdosis/hocuspocus/commit/8dbe872d2eb6a68361b4257728bf222ad60fa516)) + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + + +### Bug Fixes + +* Do not retry connection if data is too large ([#586](https://github.com/ueberdosis/hocuspocus/issues/586)) ([462a87f](https://github.com/ueberdosis/hocuspocus/commit/462a87f08bfdcaece4adb1ef8dca25ae42b3cb36)) +* npm scripts for frontend playground ([#578](https://github.com/ueberdosis/hocuspocus/issues/578)) ([b4b61a0](https://github.com/ueberdosis/hocuspocus/commit/b4b61a0fc45524ed690d1b831c2d5ae67c6a5314)) + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package hocuspocus + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + + +### Bug Fixes + +* connection.sendStateless error when connections more than one ([#556](https://github.com/ueberdosis/hocuspocus/issues/556)) ([9a9260f](https://github.com/ueberdosis/hocuspocus/commit/9a9260ffd5ee559bdc4f8ba4e7c1b1c12c2944d0)) + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package hocuspocus + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package hocuspocus + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + + +### Bug Fixes + +* make `transformer` compatible with `@tiptap/core@^2.0.0-beta.210` ([#512](https://github.com/ueberdosis/hocuspocus/issues/512)) ([591103f](https://github.com/ueberdosis/hocuspocus/commit/591103fe15d3c3760b7e59e65785a34e1d736c16)) + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package hocuspocus + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package hocuspocus + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Bug Fixes + +* Account for this.webSocket being null ([#235](https://github.com/ueberdosis/hocuspocus/issues/235)) ([b1476b6](https://github.com/ueberdosis/hocuspocus/commit/b1476b60f0eceb83d70c1ded2b3139e4f865a869)) +* demos ([59b73de](https://github.com/ueberdosis/hocuspocus/commit/59b73de474f23b563995264f87a395e65ff73fee)) +* Documentation fixes ([#138](https://github.com/ueberdosis/hocuspocus/issues/138)) ([8352196](https://github.com/ueberdosis/hocuspocus/commit/83521960dc47f063eb26a9a3162017da470ead54)), closes [#137](https://github.com/ueberdosis/hocuspocus/issues/137) +* Empty sync message causes error in client MessageReceiver ([#174](https://github.com/ueberdosis/hocuspocus/issues/174)) ([f9dca69](https://github.com/ueberdosis/hocuspocus/commit/f9dca69eb96d1ede37a0709bd3b7735bf1ff57ba)) +* fix hook promise chaining ([ee5052d](https://github.com/ueberdosis/hocuspocus/commit/ee5052d236ba0b400880dc7ca1c90cefdd372003)) +* fix playground build ([86bd9db](https://github.com/ueberdosis/hocuspocus/commit/86bd9db5f42370926b8b2027d679873b9c662b15)) +* frontend demo packages are not installed on fresh clone ([7ead9e2](https://github.com/ueberdosis/hocuspocus/commit/7ead9e22d8e5227d0913f2d95a7578479e520b4c)) +* og image domain ([5b78e3f](https://github.com/ueberdosis/hocuspocus/commit/5b78e3fa98bfc40dc109a1239297434a1af143d1)) +* Potential onCreateDocument race condition ([#167](https://github.com/ueberdosis/hocuspocus/issues/167)) ([b3e3e4d](https://github.com/ueberdosis/hocuspocus/commit/b3e3e4dea74f9b833ccb0c6a6521f55c001411c1)) +* Remote client awareness not immediately available ([37703f6](https://github.com/ueberdosis/hocuspocus/commit/37703f695f6bf3b0508c287294c5d26d2e888c37)) +* Remove event listener once unused ([#220](https://github.com/ueberdosis/hocuspocus/issues/220)) ([0422196](https://github.com/ueberdosis/hocuspocus/commit/0422196f8a4e09af530c51419742b137b9ebbc69)) +* typescript strings ([0dd5f12](https://github.com/ueberdosis/hocuspocus/commit/0dd5f1292616e426cdb4cc79e83ab8ced0895bfa)) +* webhook docs ([f0faac2](https://github.com/ueberdosis/hocuspocus/commit/f0faac268dedabca8b0afa5b485bb02af8b5f399)) + + +### Features + +* add connect event to webhook extension ([d907354](https://github.com/ueberdosis/hocuspocus/commit/d907354561ca31130061b6393c240598d67259b1)) +* Add connectionsCount and documentsCount methods to server ([8bdbcd8](https://github.com/ueberdosis/hocuspocus/commit/8bdbcd86b1f18462f6636b75a4cbd97ebefdb227)) +* add create event to import documents from api ([240a032](https://github.com/ueberdosis/hocuspocus/commit/240a032a8d839144f9662c1ec947958823abfd45)) +* add default schema to ProsemirrorTransformer and default extensions to TiptapTransformer, making both parameters in toYdoc optional ([e330ffd](https://github.com/ueberdosis/hocuspocus/commit/e330ffd81f8cbd98c1d57545d3ec9d51807f7db3)) +* add disconnect event to webhook extension ([81679dd](https://github.com/ueberdosis/hocuspocus/commit/81679dd7828e96fc6e0a279880379eb16b28d788)) +* add read only mode ([7b59d52](https://github.com/ueberdosis/hocuspocus/commit/7b59d522b966b51347db35ac6a4524211e44ae9c)) +* add request headers and parameters to onCreateDocument ([47a8b95](https://github.com/ueberdosis/hocuspocus/commit/47a8b95baf8dd22ebd71c56565420179402cdaa4)) +* add throttle extension ([b65de2a](https://github.com/ueberdosis/hocuspocus/commit/b65de2aa127c79fcad433d5e7353face7ad82d26)) +* add transformations page to the guide ([81e0ddf](https://github.com/ueberdosis/hocuspocus/commit/81e0ddfefbfdf2095e477cca8d1eab7eac576c04)) +* extension-redis should watch the ydoc for changes and notify other hocuspocus instances about it (via redis pub/sub) ([bacc535](https://github.com/ueberdosis/hocuspocus/commit/bacc53594550aa771765f8e123c9797a88071a20)) +* improve wevhook docs ([3f774c4](https://github.com/ueberdosis/hocuspocus/commit/3f774c4aec6edea4428b6d3b12a24902f1abf383)) +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8cb2084 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023, Tiptap GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..86886ea --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +> [!TIP] +> Help us **chart the future of Hocuspocus** by telling us about your needs in a short survey. Plus, you’ll get a chance to earn a $50 gift card. +> **[Take the survey →](https://docs.google.com/forms/d/e/1FAIpQLSeUS8S1iZTud58GBSD_d4c8lfUvkZHvzU99kWxP1wnNZnPKpw/viewform?usp=header)** + +# Hocuspocus +A plug & play collaboration backend based on [Y.js](https://github.com/yjs/yjs). + +[![Build Status](https://github.com/ueberdosis/hocuspocus/workflows/build/badge.svg)](https://github.com/ueberdosis/hocuspocus/actions) +[![Version](https://img.shields.io/npm/v/@hocuspocus/server.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/server) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/server.svg)](https://npmcharts.com/compare/@hocuspocus/server?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/server.svg)](https://www.npmjs.com/package/@hocuspocus/server) +[![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true)](https://discord.gg/WtJ49jGshW) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Documentation +The full documentation is a available on [hocuspocus.dev/introduction](https://tiptap.dev/docs/hocuspocus/introduction). + +## Cloud Hosting + +You want to use Hocuspocus, but don't want to care about hosting? Check our [Cloud Offering: Tiptap Collab](https://tiptap.dev/collab) + + +## Feedback +Send all your questions, feedback and bug reports to [humans@tiptap.dev](mailto:humans@tiptap.dev) or [create an issue](https://github.com/ueberdosis/hocuspocus/issues/new/choose) here. + +## Usage +The following example is a example setup you need to start a WebSocket server. By default, it’s listening on [http://127.0.0.1](http://127.0.0.1) (or prefixed with the WebSocket protocol on ws://127.0.0.1): + +```js +import { Server } from '@hocuspocus/server' +import { SQLite } from '@hocuspocus/extension-sqlite' + +const server = new Server({ + port: 1234, + + async onConnect() { + console.log('🔮') + }, + + extensions: [ + new SQLite({ + database: 'db.sqlite', + }), + ], +}); + +server.listen(); +``` + +## Community +For help, discussion about best practices, or any other conversation: + +[Join the Tiptap Discord Server](https://discord.gg/WtJ49jGshW) + +## Sponsors 💖 + + + + + + + + + + + + + + +
+ +
+ Tiptap +
+
+ +
+ Cargo +
+
+ +
+ Saga +
+
+ +
+ Gamma +
+
+ +
+ Outline +
+
+ +
+ Ahrefs +
+
+ +
+ Brickdoc +
+
+ +
+ Sana +
+
+ +
+ Poggio +
+
+ +… and hundreds of awesome inviduals. + +Using Hocuspocus in production? Invest in the future of Hocuspocus and [become a sponsor!](https://github.com/sponsors/ueberdosis) + +## Contributing +Please see [CONTRIBUTING](docs/contributing.md) for details. + +## Contributors +[kris](https://github.com/kriskbx) (who wrote the initial version), [Tom Moor](https://github.com/tommoor), [YousefED (@TypeCellOS)](https://github.com/YousefED) and [many more](../../contributors). + +## License +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/RELEASE_NOTES_V4.md b/RELEASE_NOTES_V4.md new file mode 100644 index 0000000..d1587c5 --- /dev/null +++ b/RELEASE_NOTES_V4.md @@ -0,0 +1,461 @@ +# Hocuspocus v4.0 Release Notes + +Hocuspocus v4 is a major release that brings cross-runtime support, improved type safety, and important bug fixes. This release focuses on making Hocuspocus run beyond Node.js -- on Bun, Deno, Cloudflare Workers, and Node with uWebSockets -- while improving the developer experience with generic Context typing and structured transaction origins. + +## Backward Compatibility + +A v3 provider can connect to a v4 server, and a v4 provider can connect to a v3 server. The wire protocol remains compatible in both directions: + +- **v3 provider -> v4 server**: The server accepts plain document names (no session routing key), does not require Pong responses, and handles auth messages without a provider version string. +- **v4 provider -> v3 server**: The provider defaults to `sessionAwareness: false`, so it sends plain document names. The extra version string in the auth message is ignored by the v3 server as trailing data. The provider does not require server-initiated Ping messages. +- **Session awareness caveat**: If `sessionAwareness: true` is explicitly enabled on a v4 provider connecting to a v3 server, the server will treat the composite routing key (`documentName\0sessionId`) as a literal document name, creating unintended documents. Keep `sessionAwareness: false` (the default) when connecting to a v3 server. + +## Highlights + +### Cross-Runtime Support + +Hocuspocus is no longer tied to the Node.js `ws` library. The server now uses [crossws](https://github.com/unjs/crossws), a universal WebSocket adapter, enabling Hocuspocus to run on: + +- **Node.js** (with `ws` or `uWebSockets.js`) +- **Bun** +- **Deno** +- **Cloudflare Workers** + +The built-in `Server` class continues to work as before for Node.js users. For other runtimes, use `Hocuspocus` directly with `handleConnection()`, which now accepts any `WebSocketLike` object and a web-standard `Request`. + +### Generic Context Type + +All core classes and hook payloads now accept a generic `Context` type parameter, enabling end-to-end type safety: + +```typescript +interface MyContext { + userId: string; + permissions: string[]; +} + +const server = Server.configure({ + async onAuthenticate({ context, token }) { + // context is typed as MyContext + return { userId: '123', permissions: ['read', 'write'] }; + }, + async onChange({ context }) { + // context.userId is typed as string + console.log(context.userId); + }, +}); +``` + +The generic defaults to `any`, so existing code without explicit typing continues to work. + +### Ordered Message Processing + +Document updates are now processed sequentially in the order they are received. Previously, concurrent messages could be processed out of order if async hooks were involved. A new internal message queue ensures CRDT updates are applied consistently. + +### Web Standard Request/Headers + +Hook payloads now use the web-standard `Request` and `Headers` objects instead of Node.js `IncomingMessage` and `IncomingHttpHeaders`. This aligns with the cross-runtime goal and provides a consistent API across all environments. + +## New Features + +### Server + +- **Cross-runtime WebSocket support** via `crossws` -- Bun, Deno, Cloudflare Workers, Node/uWebSockets all supported *(non-breaking)* +- **Generic `Context` type parameter** on `Hocuspocus`, `Server`, `Extension`, `Connection`, `ClientConnection`, `DirectConnection`, and all hook payloads *(non-breaking -- defaults to `any`)* +- **Structured transaction origins** -- new `TransactionOrigin` union type (`ConnectionTransactionOrigin | RedisTransactionOrigin | LocalTransactionOrigin`) with helper functions `isTransactionOrigin()` and `shouldSkipStoreHooks()` *(breaking -- see upgrade guide)* +- **`onLoadDocument` now accepts `Uint8Array` returns** -- extensions can return raw Yjs updates instead of constructing a full `Y.Doc`, simplifying storage extensions *(non-breaking)* +- **`handleConnection()` returns `ClientConnection`** -- enables programmatic access to the connection lifecycle for custom integrations *(non-breaking)* +- **Ordered message processing** -- messages are queued and processed sequentially per connection *(non-breaking)* +- **Session awareness** -- the server supports session-aware multiplexing, allowing multiple providers with the same document name on a single WebSocket. Each provider gets a unique `sessionId` routed via a composite key. The server transparently falls back to plain document names for v3 providers *(non-breaking)* +- **Auth retry support** -- failed authentication now properly cleans up state, allowing clients to retry without reconnecting *(non-breaking)* +- **DirectConnection context** -- `openDirectConnection(documentName, context)` now accepts and propagates a context object *(non-breaking)* +- **Store hooks on all changes** -- `onStoreDocument` is now triggered on any document change (not just WebSocket-originated ones), with explicit opt-out via `skipStoreHooks` on `LocalTransactionOrigin` *(non-breaking)* +- **Provider version awareness** -- the provider version is available on `Connection.providerVersion` and in hook payloads (`onConnect`, `onAuthenticate`, `connected`), making it easier to introduce protocol changes in a backward-compatible way *(non-breaking)* +- **`SkipFurtherHooksError`** -- extensions can throw `SkipFurtherHooksError` (from `@hocuspocus/common`) in `onStoreDocument` to signal that persistence was handled and remaining hooks should be skipped *(non-breaking)* + +### Provider + +- **Session awareness** -- new `sessionAwareness` option (default: `false`). When enabled, the provider embeds a unique `sessionId` in the document name field of every message, enabling multiple providers with the same document name on one WebSocket. Keep disabled when connecting to a v3 server *(non-breaking)* +- **Provider version sent during auth** -- the provider now sends its package version to the server in the authentication message. The extra field is safely ignored by v3 servers *(non-breaking)* +- **Awareness message deduplication** -- when the WebSocket is not yet open and messages are queued, duplicate awareness messages for the same document are deduplicated, keeping only the latest one *(non-breaking)* +- **Application-level Ping/Pong** -- new `MessageType.Ping` (9) and `MessageType.Pong` (10). The provider responds to server Ping messages with Pong, replacing WebSocket-level ping/pong which is not available in all runtimes. The provider works fine without receiving Pings (e.g., when connected to a v3 server) *(non-breaking)* +- **`ws` package types removed** -- the provider no longer imports `Event`, `MessageEvent`, or `CloseEvent` from the `ws` package. It uses web-standard types and types from `@hocuspocus/common` instead *(breaking for TypeScript users who relied on `ws` types being re-exported)* +- **`CloseEvent` shape simplified** -- the `CloseEvent` passed to `onClose` callbacks no longer includes `target` and `type` fields. Only `code` and `reason` remain *(breaking if your `onClose` handler reads `event.target` or `event.type`)* +- **Attach collision detection** -- `HocuspocusProviderWebsocket.attach()` now throws an error if you try to attach two authenticated providers with the same effective name. Previously it silently overwrote the existing provider *(non-breaking for correct usage; may surface existing bugs)* +- **Unknown message types handled gracefully** -- unknown message types now log `console.error` instead of throwing. This makes rolling out future protocol additions easier *(non-breaking)* + +## Bug Fixes + +### Server + +- **Auth state reset on failure** -- when authentication fails, document state is cleaned up so the client can send a new auth message without reconnecting (#944) *(non-breaking)* +- **`onLoadDocument` accepts `Uint8Array`** -- the callback now correctly handles both `Y.Doc` and `Uint8Array` returns (#795, #271) *(non-breaking)* +- **Close code type check** -- close event codes are now properly validated as numbers (#1062) *(non-breaking)* +- **Store hooks reliability** -- `onStoreDocument` now triggers on any document change, preventing accidental data loss when updates lacked a Yjs origin *(non-breaking)* +- **`onStoreDocument` payload type** -- the Database extension and Logger extension now correctly type the parameter as `onStoreDocumentPayload` instead of the incorrect `onChangePayload` / `onDisconnectPayload` *(non-breaking)* +- **Document name validation** -- empty and whitespace-only document names are now rejected on both WebSocket connections and direct connections *(non-breaking)* +- **Store hook retry on failure** -- when `onStoreDocument` hooks throw, the document stays in memory and retries are attempted to avoid data loss *(non-breaking)* +- **Graceful shutdown flushes pending stores** -- `Server.destroy()` now immediately executes all pending debounced `onStoreDocument` calls, ensuring documents are persisted before the server exits (even when `unloadImmediately: false`) *(non-breaking)* +- **Memory optimization** -- outgoing Yjs update messages are now encoded once and shared across connections instead of being re-created per connection *(non-breaking)* + +### Provider + +- **Unknown message types no longer crash the provider** -- `console.error` instead of `throw`. This makes future protocol additions easier *(non-breaking)* + +## Infrastructure Changes + +- **Package manager**: migrated from npm to **pnpm** workspaces +- **Bundler**: migrated from Rollup to **Rolldown** +- **SQLite extension**: migrated from `sqlite3` to **better-sqlite3** (synchronous API, actively maintained) +- **Node.js requirement**: `>=22` (specified in `@hocuspocus/server`) +- **Default timeout**: increased from 30s to 60s +- **Lerna**: upgraded to v9 with pnpm as npm client + +--- + +# Upgrade Guide: v3 to v4 + +## 1. Update Dependencies + +```bash +# Install v4 +npm install @hocuspocus/server@^4.0.0 @hocuspocus/provider@^4.0.0 +``` + +If you use the SQLite extension: + +```bash +npm uninstall sqlite3 +npm install @hocuspocus/extension-sqlite@^4.0.0 better-sqlite3 +npm install -D @types/better-sqlite3 +``` + +**Node.js requirement**: v4 requires Node.js 22 or later. + +## Server Changes + +### 2. Request and Headers (Breaking) + +All hook payloads now use web-standard `Request` and `Headers` instead of Node.js `IncomingMessage` and `IncomingHttpHeaders`. + +#### Before (v3) + +```typescript +async onAuthenticate({ request, requestHeaders }) { + const token = requestHeaders['authorization']; + const ip = requestHeaders['x-forwarded-for']; + const url = request.url; +} +``` + +#### After (v4) + +```typescript +async onAuthenticate({ request, requestHeaders }) { + const token = requestHeaders.get('authorization'); + const ip = requestHeaders.get('x-forwarded-for'); + const url = request.url; +} +``` + +**Key differences:** +- `requestHeaders['key']` becomes `requestHeaders.get('key')` +- `request.socket.remoteAddress` is no longer available -- use `x-forwarded-for` or `x-real-ip` headers from your reverse proxy +- `request` is now a web `Request` object, not Node.js `IncomingMessage` + +**Note:** The `onUpgrade` and `onRequest` hooks still use Node.js `IncomingMessage`/`ServerResponse` since they operate at the HTTP level before the WebSocket upgrade. + +### 3. `onStoreDocument` Payload (Breaking) + +The `onStoreDocument` and `afterStoreDocument` payloads have been restructured. Several fields that were tied to a specific connection have been removed, since store hooks can now be triggered by non-connection sources. + +#### Before (v3) + +```typescript +async onStoreDocument({ + context, + requestHeaders, + requestParameters, + socketId, + transactionOrigin, + document, + documentName, + clientsCount, + instance, +}) { + // ... +} +``` + +#### After (v4) + +```typescript +async onStoreDocument({ + lastContext, // was: context + lastTransactionOrigin, // was: transactionOrigin + document, + documentName, + clientsCount, + instance, + // removed: requestHeaders, requestParameters, socketId +}) { + // ... +} +``` + +**Migration:** +- `context` → `lastContext` +- `transactionOrigin` → `lastTransactionOrigin` +- `requestHeaders`, `requestParameters`, `socketId` -- removed. If you need these, access them from the `lastContext`. Note that they contain the context of the last connection that triggered the hook. + +### 4. `onAwarenessUpdate` Payload (Breaking) + +The `onAwarenessUpdate` payload has been simplified. Connection-specific fields are removed and replaced with the source of the update. + +#### Before (v3) + +```typescript +async onAwarenessUpdate({ + context, + requestHeaders, + requestParameters, + socketId, + document, + documentName, + added, updated, removed, states, +}) { + // ... +} +``` + +#### After (v4) + +```typescript +async onAwarenessUpdate({ + transactionOrigin, // new: structured origin + connection, // new: optional, the connection that triggered the update + document, + documentName, + added, updated, removed, + awareness, // new: the Awareness instance + states, + // removed: context, requestHeaders, requestParameters, socketId +}) { + // Access context through the connection if needed + const context = connection?.context; +} +``` + +### 5. WebSocket Type Changes (Breaking) + +If your code references the `WebSocket` type from the `ws` package, update to `WebSocketLike`: + +#### Before (v3) + +```typescript +import { WebSocket } from 'ws'; + +// In your code +const ws: WebSocket = connection.webSocket; +``` + +#### After (v4) + +```typescript +import type { WebSocketLike } from '@hocuspocus/server'; + +// In your code +const ws: WebSocketLike = connection.webSocket; +``` + +The `WebSocketLike` interface is minimal: + +```typescript +interface WebSocketLike { + send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; + close(code?: number, reason?: string): void; + readyState: number; +} +``` + +### 6. Server Constructor (Breaking) + +WebSocket options are now passed inside the configuration object instead of as a separate parameter. + +#### Before (v3) + +```typescript +import { Server } from '@hocuspocus/server'; + +const server = new Server( + { port: 8080, extensions: [...] }, + { maxPayload: 1024 * 1024 } // ws options as 2nd arg +); +``` + +#### After (v4) + +```typescript +import { Server } from '@hocuspocus/server'; + +const server = new Server({ + port: 8080, + extensions: [...], + websocketOptions: { maxPayload: 1024 * 1024 }, +}); +``` + +`Server.configure()` still works the same way -- just move `websocketOptions` into the config. + +### 7. Transaction Origin Changes (Breaking) + +If you inspect `transactionOrigin` in hooks (e.g., in `onChange`), the format has changed from raw values to structured objects. + +#### Before (v3) + +```typescript +async onChange({ transactionOrigin }) { + if (transactionOrigin === '__hocuspocus__redis__origin__') { + // came from Redis + } + if (transactionOrigin instanceof Connection) { + // came from a client connection + } +} +``` + +#### After (v4) + +```typescript +import { isTransactionOrigin, shouldSkipStoreHooks } from '@hocuspocus/server'; + +async onChange({ transactionOrigin }) { + if (isTransactionOrigin(transactionOrigin)) { + switch (transactionOrigin.source) { + case 'redis': + // came from Redis + break; + case 'connection': + // came from a client connection + // transactionOrigin.connection is available + break; + case 'local': + // came from server-side code (e.g., DirectConnection) + break; + } + } +} +``` + +### 8. SQLite Extension (Breaking) + +The SQLite extension has been migrated from the unmaintained `sqlite3` package to `better-sqlite3`. + +**What you need to do:** +1. Replace the `sqlite3` dependency with `better-sqlite3` +2. No changes to your Hocuspocus configuration -- the extension API is the same +3. Existing SQLite database files are fully compatible (no data migration needed) + +If you wrote a **custom schema** for the SQLite extension, note that `better-sqlite3` uses named parameters without the `$` prefix: +- `$name` → `name` +- `$data` → `data` + +### 9. Custom `handleConnection` Integrations (Breaking) + +If you call `handleConnection()` directly (e.g., for Express/Koa integration), the signature has changed: + +#### Before (v3) + +```typescript +import { IncomingMessage } from 'http'; + +wss.on('connection', (ws: WebSocket, request: IncomingMessage) => { + hocuspocus.handleConnection(ws, request, context); +}); +``` + +#### After (v4) + +```typescript +wss.on('connection', (ws, request: Request) => { + const clientConnection = hocuspocus.handleConnection(ws, request, context); + // clientConnection is now returned for programmatic access +}); +``` + +**Key changes:** +- `request` must be a web-standard `Request` (not `IncomingMessage`) +- The method now returns a `ClientConnection` instance +- The WebSocket no longer needs to be from the `ws` package -- any `WebSocketLike` works +- You are responsible for calling `clientConnection.handleMessage(data)` and `clientConnection.handleClose(event)` if you're not using the built-in `Server` class + +### 10. Timeout Change (Non-Breaking) + +The default connection timeout has increased from 30 seconds to 60 seconds. If you relied on the old default, you can restore it: + +```typescript +const server = Server.configure({ + timeout: 30_000, +}); +``` + +## Provider Changes + +### 11. `CloseEvent` Shape (Breaking) + +The `CloseEvent` passed to `onClose` callbacks no longer includes `target` and `type` fields. Only `code` and `reason` remain. + +#### Before (v3) + +```typescript +onClose({ event }) { + console.log(event.code, event.reason, event.target, event.type); +} +``` + +#### After (v4) + +```typescript +onClose({ event }) { + console.log(event.code, event.reason); + // event.target and event.type are no longer available +} +``` + +### 12. `ws` Package Types Removed (Breaking -- TypeScript Only) + +The provider no longer imports or re-exports `Event`, `MessageEvent`, or `CloseEvent` from the `ws` package. If your TypeScript code relied on these types being available through the provider, import them from `@hocuspocus/common` or use web-standard types instead. + +### 13. Session Awareness (Non-Breaking) + +The provider has a new `sessionAwareness` option (default: `false`). When enabled, it embeds a unique `sessionId` in the document name field of every message, allowing multiple providers with the same document name on a single WebSocket. + +No action is needed -- the default is `false`, which preserves v3 behavior. Enable it only when connecting to a v4 server and you need session multiplexing: + +```typescript +const provider = new HocuspocusProvider({ + url: 'ws://localhost:1234', + name: 'my-document', + sessionAwareness: true, // opt-in for v4 server multiplexing +}); +``` + +## Summary Checklist + +### Server +- [ ] Update to Node.js 22+ +- [ ] Update all `@hocuspocus/*` packages to v4 +- [ ] Replace `requestHeaders['key']` with `requestHeaders.get('key')` in all hooks +- [ ] Replace `request.socket.remoteAddress` with proxy headers (`x-forwarded-for`) +- [ ] Update `onStoreDocument` handlers: `context` -> `lastContext`, remove `requestHeaders`/`requestParameters`/`socketId` +- [ ] Update `onAwarenessUpdate` handlers if used +- [ ] Replace `WebSocket` type imports from `ws` with `WebSocketLike` from `@hocuspocus/server` +- [ ] Move `websocketOptions` into the `Server` configuration object +- [ ] Update transaction origin checks to use `isTransactionOrigin()` and `.source` +- [ ] If using SQLite extension: replace `sqlite3` with `better-sqlite3` +- [ ] If using custom `handleConnection`: update to new signature and `Request` type + +### Provider +- [ ] Update `@hocuspocus/provider` to v4 +- [ ] Remove references to `event.target` / `event.type` in `onClose` handlers +- [ ] Update any TypeScript imports that relied on `ws` types from the provider +- [ ] Test your extensions and hooks thoroughly diff --git a/babel.config.cjs b/babel.config.cjs new file mode 100644 index 0000000..a892c5b --- /dev/null +++ b/babel.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + presets: [ + '@babel/preset-env', + ], + plugins: [ + '@babel/plugin-transform-class-properties', + ], +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b1f8889 --- /dev/null +++ b/biome.json @@ -0,0 +1,18 @@ +{ + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "linter": { + "rules": { + "suspicious": { + "noAsyncPromiseExecutor": "off", + "noExplicitAny": "off" + }, + "complexity": { + "noForEach": "off" + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3169dbe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" + +services: + redis: + image: "redis:6-alpine" + ports: + - "6379:6379" diff --git a/docsearch.config.json b/docsearch.config.json new file mode 100644 index 0000000..a816ee6 --- /dev/null +++ b/docsearch.config.json @@ -0,0 +1,37 @@ +{ + "index_name": "hocuspocus", + "start_urls": [ + "https://tiptap.dev/docs/hocuspocus" + ], + "sitemap_alternate_links": true, + "selectors": { + "default": { + "lvl0": { + "selector": "", + "global": true, + "default_value": "Documentation" + }, + "lvl1": "main h1", + "lvl2": "main h2", + "lvl3": "main h3", + "lvl4": "main h4", + "lvl5": "main h5", + "lvl6": "main h6", + "text": "main p, main li, main pre, main td" + } + }, + "strip_chars": " .,;:#", + "custom_settings": { + "separatorsToIndex": "_", + "attributesForFaceting": [ + "type", + "lang" + ], + "attributesToRetrieve": [ + "hierarchy", + "text", + "anchor", + "url" + ] + } +} diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..e5ee0dd --- /dev/null +++ b/lerna.json @@ -0,0 +1,13 @@ +{ + "packages": [ + "packages/*" + ], + "npmClient": "pnpm", + "command": { + "publish": { + "conventionalCommits": true + } + }, + "version": "4.7.0", + "$schema": "node_modules/lerna/schemas/lerna-schema.json" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..87cc8b1 --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "private": true, + "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee", + "browserslist": [ + "defaults", + "not IE 11", + "maintained node versions" + ], + "type": "module", + "ava": { + "files": [ + "tests/**/*", + "!tests/utils/**/*", + "!database/**/*" + ], + "extensions": { + "ts": "module" + }, + "require": [ + "./tests/utils/_force-exit.ts" + ], + "nodeArguments": [ + "--no-warnings", + "--conditions=source", + "--experimental-transform-types", + "--experimental-specifier-resolution=node" + ], + "timeout": "30s", + "workerThreads": false + }, + "scripts": { + "start": "pnpm playground", + "playground": "pnpm playground:default", + "playground:load-document": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/load-document.ts\"", + "playground:express": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/express.ts\"", + "playground:koa": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/koa.ts\"", + "playground:default": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/default.ts\"", + "playground:slow": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/slow.ts\"", + "playground:redis": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/redis.ts\"", + "playground:webhook": "concurrently --kill-others \"pnpm -C ./playground/frontend dev\" \"pnpm -C ./playground/backend dev src/webhook.ts\"", + "lint:ts": "tsc --noEmit -p tsconfig.json --composite false", + "lint": "biome lint && pnpm lint:ts", + "lint:fix": "biome lint --fix", + "test": "ava", + "test:watch": "ava --watch", + "build:packages": "pnpm clean:packages && rolldown -c", + "build:watch": "pnpm clean:packages && rolldown --watch -c", + "clean:packages": "rm -Rf ./packages/*/dist", + "release:major": "pnpm build:packages && lerna version major --force-publish --sign-git-tag --sign-git-commit", + "release:major:pre": "pnpm build:packages && lerna version premajor --force-publish --preid rc --sign-git-tag --sign-git-commit", + "release:minor": "pnpm build:packages && lerna version minor --force-publish --sign-git-tag --sign-git-commit", + "release:minor:pre": "pnpm build:packages && lerna version preminor --force-publish --preid rc --sign-git-tag --sign-git-commit", + "release:patch": "pnpm build:packages && lerna version patch --force-publish --sign-git-tag --sign-git-commit", + "release:patch:pre": "pnpm build:packages && lerna version prepatch --force-publish --preid rc --sign-git-tag --sign-git-commit", + "release:pre": "pnpm build:packages && lerna version prerelease --force-publish --preid rc --sign-git-tag --sign-git-commit", + "publish:latest": "pnpm build:packages && lerna publish from-package --yes", + "publish:pre": "pnpm build:packages && lerna publish from-package --yes --dist-tag next", + "reset": "pnpm clean:packages && rm -Rf ./**/.cache && rm -Rf ./**/.temp && rm -Rf ./**/node_modules && rm -Rf ./pnpm-lock.yaml && pnpm install", + "ncu": "ncu -i --deep && pnpm install" + }, + "devDependencies": { + "@biomejs/biome": "2.4.5", + "@types/bun": "^1.3.9", + "ava": "^7.0.0", + "concurrently": "^6.5.1", + "lerna": "^9.0.5", + "nx": "^22.7.8", + "rolldown": "1.0.0-rc.6", + "rolldown-plugin-dts": "^0.22.3", + "typescript": "5.8.2" + }, + "engines": { + "node": ">=22" + }, + "name": "hocuspocus", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + } +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..ebd4519 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,1273 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.22...@hocuspocus/cli@1.0.0-alpha.23) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.21...@hocuspocus/cli@1.0.0-alpha.22) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.20...@hocuspocus/cli@1.0.0-alpha.21) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.19...@hocuspocus/cli@1.0.0-alpha.20) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.18...@hocuspocus/cli@1.0.0-alpha.19) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.17...@hocuspocus/cli@1.0.0-alpha.18) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.16...@hocuspocus/cli@1.0.0-alpha.17) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.15...@hocuspocus/cli@1.0.0-alpha.16) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.14...@hocuspocus/cli@1.0.0-alpha.15) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.13...@hocuspocus/cli@1.0.0-alpha.14) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.12...@hocuspocus/cli@1.0.0-alpha.13) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.11...@hocuspocus/cli@1.0.0-alpha.12) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.10...@hocuspocus/cli@1.0.0-alpha.11) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.9...@hocuspocus/cli@1.0.0-alpha.10) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.8...@hocuspocus/cli@1.0.0-alpha.9) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.7...@hocuspocus/cli@1.0.0-alpha.8) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.6...@hocuspocus/cli@1.0.0-alpha.7) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.5...@hocuspocus/cli@1.0.0-alpha.6) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.4...@hocuspocus/cli@1.0.0-alpha.5) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.3...@hocuspocus/cli@1.0.0-alpha.4) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.2...@hocuspocus/cli@1.0.0-alpha.3) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# [1.0.0-alpha.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/cli@1.0.0-alpha.1...@hocuspocus/cli@1.0.0-alpha.2) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/cli + + + + + +# 1.0.0-alpha.1 (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/cli diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..833007f --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,18 @@ +# @hocuspocus/cli +[![Version](https://img.shields.io/npm/v/@hocuspocus/cli.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/cli) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/cli.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/cli.svg)](https://www.npmjs.com/package/@hocuspocus/cli) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Introduction +A CLI tool to start a local [Hocuspocus](https://github.com/ueberdosis/hocuspocus) server – useful for quickly spinning up a collaboration backend for development, demos, or simple deployments, with optional SQLite, S3, logger, and webhook extensions. + +```bash +npx @hocuspocus/cli +``` + +## Official Documentation +Documentation can be found in the [GitHub repository](https://github.com/ueberdosis/hocuspocus). + +## License +Hocuspocus is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..a5d0fcd --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,35 @@ +{ + "name": "@hocuspocus/cli", + "description": "a CLI tool to start a local Hocuspocus server", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "yjs", + "yjs-websocket" + ], + "license": "MIT", + "type": "module", + "bin": { + "hocuspocus": "./src/index.js", + "@hocuspocus/cli": "./src/index.js" + }, + "files": [ + "src" + ], + "dependencies": { + "@hocuspocus/extension-logger": "workspace:^", + "@hocuspocus/extension-s3": "workspace:^", + "@hocuspocus/extension-sqlite": "workspace:^", + "@hocuspocus/extension-webhook": "workspace:^", + "@hocuspocus/server": "workspace:^", + "meow": "^14.0.0" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js new file mode 100755 index 0000000..f80d45f --- /dev/null +++ b/packages/cli/src/index.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node + +import { Logger } from "@hocuspocus/extension-logger"; +import { S3 } from "@hocuspocus/extension-s3"; +import { SQLite } from "@hocuspocus/extension-sqlite"; +import { Webhook } from "@hocuspocus/extension-webhook"; +import { Server } from "@hocuspocus/server"; +import meow from "meow"; + +export const cli = meow( + ` + Usage + $ hocuspocus [options] + + Options + --port=, -p Set the port, defaults to 1234. + --webhook=, -w Configure a custom webhook. + --sqlite=, -s Store data in a SQLite database, defaults to :memory:. + --s3 Store data in S3 or S3-compatible storage. + --s3-bucket= S3 bucket name (required when using --s3). + --s3-region= S3 region, defaults to us-east-1. + --s3-prefix= S3 key prefix for documents. + --s3-endpoint= S3 endpoint URL (for S3-compatible services like MinIO). + --version Show the current version number. + + Examples + $ hocuspocus --port 8080 + $ hocuspocus --webhook http://localhost/webhooks/hocuspocus + $ hocuspocus --sqlite + $ hocuspocus --sqlite database/default.sqlite + $ hocuspocus --s3 --s3-bucket my-docs + $ hocuspocus --s3 --s3-bucket my-docs --s3-region eu-west-1 + $ hocuspocus --s3 --s3-bucket my-docs --s3-endpoint http://localhost:9000 + + Environment Variables (for S3) + AWS_ACCESS_KEY_ID AWS access key ID + AWS_SECRET_ACCESS_KEY AWS secret access key + AWS_REGION AWS region (alternative to --s3-region) +`, + { + importMeta: import.meta, + flags: { + port: { + type: "string", + shortFlag: "p", + default: "1234", + }, + webhook: { + type: "string", + shortFlag: "w", + default: "", + }, + sqlite: { + type: "string", + shortFlag: "s", + default: "", + }, + s3: { + type: "boolean", + default: false, + }, + s3Bucket: { + type: "string", + default: "", + }, + s3Region: { + type: "string", + default: "us-east-1", + }, + s3Prefix: { + type: "string", + default: "", + }, + s3Endpoint: { + type: "string", + default: "", + }, + }, + }, +); + +export const getConfiguredWebhookExtension = () => { + return cli.flags.webhook + ? new Webhook({ + url: cli.flags.webhook, + }) + : undefined; +}; + +export const getConfiguredSQLiteExtension = () => { + if (cli.flags.sqlite) { + return new SQLite({ + database: cli.flags.sqlite, + }); + } + if (process.argv.includes("--sqlite")) { + return new SQLite(); + } + + return undefined; +}; + +export const getConfiguredS3Extension = () => { + if (!cli.flags.s3) { + return undefined; + } + + const bucket = cli.flags.s3Bucket || process.env.S3_BUCKET; + if (!bucket) { + console.error( + "❌ S3 bucket is required. Use --s3-bucket or set S3_BUCKET environment variable.", + ); + process.exit(1); + } + + const config = { + bucket, + region: cli.flags.s3Region || process.env.AWS_REGION || "us-east-1", + }; + + if (cli.flags.s3Prefix) { + config.prefix = cli.flags.s3Prefix; + } + + if (cli.flags.s3Endpoint) { + config.endpoint = cli.flags.s3Endpoint; + config.forcePathStyle = true; + } + + if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { + config.credentials = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }; + } + + return new S3(config); +}; + +const server = new Server({ + port: Number.parseInt(cli.flags.port, 10), + extensions: [ + new Logger(), + getConfiguredWebhookExtension(), + getConfiguredSQLiteExtension(), + getConfiguredS3Extension(), + ].filter((extension) => extension), +}); + +server.listen(); diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md new file mode 100644 index 0000000..c32bdb2 --- /dev/null +++ b/packages/common/CHANGELOG.md @@ -0,0 +1,1195 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + + +### Features + +* optimize retry logic ([302eb08](https://github.com/ueberdosis/hocuspocus/commit/302eb08d9a2a2b59d44424571fdc70d5790d8fd8)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + + +### Features + +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + + +### Bug Fixes + +* strict types for authorizedScope ([#1029](https://github.com/ueberdosis/hocuspocus/issues/1029)) ([44e1b5d](https://github.com/ueberdosis/hocuspocus/commit/44e1b5d19f632bf646b654efd2c2ba70587adf62)) + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + + +### Features + +* add onTokenSync hook for auth token re-sync ([#1001](https://github.com/ueberdosis/hocuspocus/issues/1001)) ([ee252fb](https://github.com/ueberdosis/hocuspocus/commit/ee252fb99ea6faf80cc71d2e1ce51726e698b76b)) + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + + +### Bug Fixes + +* Do not retry connection if data is too large ([#586](https://github.com/ueberdosis/hocuspocus/issues/586)) ([462a87f](https://github.com/ueberdosis/hocuspocus/commit/462a87f08bfdcaece4adb1ef8dca25ae42b3cb36)) + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.11...@hocuspocus/common@1.0.0-alpha.12) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.10...@hocuspocus/common@1.0.0-alpha.11) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.9...@hocuspocus/common@1.0.0-alpha.10) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.8...@hocuspocus/common@1.0.0-alpha.9) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.7...@hocuspocus/common@1.0.0-alpha.8) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.6...@hocuspocus/common@1.0.0-alpha.7) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.5...@hocuspocus/common@1.0.0-alpha.6) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.4...@hocuspocus/common@1.0.0-alpha.5) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.3...@hocuspocus/common@1.0.0-alpha.4) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.2...@hocuspocus/common@1.0.0-alpha.3) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# [1.0.0-alpha.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/common@1.0.0-alpha.1...@hocuspocus/common@1.0.0-alpha.2) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/common + + + + + +# 1.0.0-alpha.1 (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/common diff --git a/packages/common/README.md b/packages/common/README.md new file mode 100644 index 0000000..82b5b7e --- /dev/null +++ b/packages/common/README.md @@ -0,0 +1,14 @@ +# @hocuspocus/common +[![Version](https://img.shields.io/npm/v/@hocuspocus/common.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/common) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/common.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/common.svg)](https://www.npmjs.com/package/@hocuspocus/common) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Introduction +Shared types and utilities used across [Hocuspocus](https://github.com/ueberdosis/hocuspocus) packages – including message/sync protocol helpers and common enums. This package is an internal building block; most users will depend on `@hocuspocus/server` or `@hocuspocus/provider` instead. + +## Official Documentation +Documentation can be found in the [GitHub repository](https://github.com/ueberdosis/hocuspocus). + +## License +Hocuspocus is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 0000000..b50bf95 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,38 @@ +{ + "name": "@hocuspocus/common", + "description": "shared code for multiple Hocuspocus packages", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-common.cjs", + "module": "dist/hocuspocus-common.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-common.esm.js", + "require": "./dist/hocuspocus-common.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "gitHead": "cd788b6a315f608ef531524409abdce1e6790726", + "dependencies": { + "lib0": "^0.2.117" + }, + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/common/src/CloseEvents.ts b/packages/common/src/CloseEvents.ts new file mode 100644 index 0000000..1c1393b --- /dev/null +++ b/packages/common/src/CloseEvents.ts @@ -0,0 +1,49 @@ +export interface CloseEvent { + code: number; + reason: string; +} + +/** + * The server is terminating the connection because a data frame was received + * that is too large. + * See: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code + */ +export const MessageTooBig: CloseEvent = { + code: 1009, + reason: "Message Too Big", +}; + +/** + * The server successfully processed the request, asks that the requester reset + * its document view, and is not returning any content. + */ +export const ResetConnection: CloseEvent = { + code: 4205, + reason: "Reset Connection", +}; + +/** + * Similar to Forbidden, but specifically for use when authentication is required and has + * failed or has not yet been provided. + */ +export const Unauthorized: CloseEvent = { + code: 4401, + reason: "Unauthorized", +}; + +/** + * The request contained valid data and was understood by the server, but the server + * is refusing action. + */ +export const Forbidden: CloseEvent = { + code: 4403, + reason: "Forbidden", +}; + +/** + * The server timed out waiting for the request. + */ +export const ConnectionTimeout: CloseEvent = { + code: 4408, + reason: "Connection Timeout", +}; diff --git a/packages/common/src/SkipFurtherHooksError.ts b/packages/common/src/SkipFurtherHooksError.ts new file mode 100644 index 0000000..20efb53 --- /dev/null +++ b/packages/common/src/SkipFurtherHooksError.ts @@ -0,0 +1,6 @@ +export class SkipFurtherHooksError extends Error { + constructor(message?: string) { + super(message ?? "Further hooks skipped"); + this.name = "SkipFurtherHooksError"; + } +} diff --git a/packages/common/src/auth.ts b/packages/common/src/auth.ts new file mode 100644 index 0000000..d62dfc6 --- /dev/null +++ b/packages/common/src/auth.ts @@ -0,0 +1,62 @@ +import * as encoding from "lib0/encoding"; +import * as decoding from "lib0/decoding"; +import type {AuthorizedScope} from "../../provider/src" + +export enum AuthMessageType { + Token = 0, + PermissionDenied = 1, + Authenticated = 2, +} + +export const writeAuthentication = ( + encoder: encoding.Encoder, + auth: string, +) => { + encoding.writeVarUint(encoder, AuthMessageType.Token); + encoding.writeVarString(encoder, auth); +}; + +export const writePermissionDenied = ( + encoder: encoding.Encoder, + reason: string, +) => { + encoding.writeVarUint(encoder, AuthMessageType.PermissionDenied); + encoding.writeVarString(encoder, reason); +}; + +export const writeAuthenticated = ( + encoder: encoding.Encoder, + scope: AuthorizedScope, +) => { + encoding.writeVarUint(encoder, AuthMessageType.Authenticated); + encoding.writeVarString(encoder, scope); +}; + +export const writeTokenSyncRequest = ( + encoder: encoding.Encoder, +) => { + encoding.writeVarUint(encoder, AuthMessageType.Token); +}; + +export const readAuthMessage = ( + decoder: decoding.Decoder, + sendToken: () => void, + permissionDeniedHandler: (reason: string) => void, + authenticatedHandler: (scope: string) => void, +) => { + switch (decoding.readVarUint(decoder)) { + case AuthMessageType.Token: { + sendToken(); + break; + } + case AuthMessageType.PermissionDenied: { + permissionDeniedHandler(decoding.readVarString(decoder)); + break; + } + case AuthMessageType.Authenticated: { + authenticatedHandler(decoding.readVarString(decoder)); + break; + } + default: + } +}; diff --git a/packages/common/src/awarenessStatesToArray.ts b/packages/common/src/awarenessStatesToArray.ts new file mode 100644 index 0000000..6ddf756 --- /dev/null +++ b/packages/common/src/awarenessStatesToArray.ts @@ -0,0 +1,10 @@ +export const awarenessStatesToArray = ( + states: Map>, +) => { + return Array.from(states.entries()).map(([key, value]) => { + return { + clientId: key, + ...value, + }; + }); +}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts new file mode 100644 index 0000000..27be2d6 --- /dev/null +++ b/packages/common/src/index.ts @@ -0,0 +1,6 @@ +export * from "./auth.ts"; +export * from "./CloseEvents.ts"; +export * from "./awarenessStatesToArray.ts"; +export * from "./routingKey.ts"; +export * from "./SkipFurtherHooksError.ts"; +export * from "./types.ts"; diff --git a/packages/common/src/routingKey.ts b/packages/common/src/routingKey.ts new file mode 100644 index 0000000..b9bb422 --- /dev/null +++ b/packages/common/src/routingKey.ts @@ -0,0 +1,11 @@ +const SEPARATOR = '\0'; + +export function makeRoutingKey(documentName: string, sessionId: string): string { + return documentName + SEPARATOR + sessionId; +} + +export function parseRoutingKey(key: string): { documentName: string; sessionId: string | null } { + const idx = key.indexOf(SEPARATOR); + if (idx === -1) return { documentName: key, sessionId: null }; + return { documentName: key.substring(0, idx), sessionId: key.substring(idx + 1) }; +} diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts new file mode 100644 index 0000000..76e79fc --- /dev/null +++ b/packages/common/src/types.ts @@ -0,0 +1,10 @@ +/** + * State of the WebSocket connection. + * https://developer.mozilla.org/de/docs/Web/API/WebSocket/readyState + */ +export enum WsReadyStates { + Connecting = 0, + Open = 1, + Closing = 2, + Closed = 3, +} diff --git a/packages/extension-database/CHANGELOG.md b/packages/extension-database/CHANGELOG.md new file mode 100644 index 0000000..7b2f929 --- /dev/null +++ b/packages/extension-database/CHANGELOG.md @@ -0,0 +1,1260 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.20...@hocuspocus/extension-database@1.0.0-alpha.21) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.19...@hocuspocus/extension-database@1.0.0-alpha.20) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.18...@hocuspocus/extension-database@1.0.0-alpha.19) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.17...@hocuspocus/extension-database@1.0.0-alpha.18) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.16...@hocuspocus/extension-database@1.0.0-alpha.17) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.15...@hocuspocus/extension-database@1.0.0-alpha.16) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.14...@hocuspocus/extension-database@1.0.0-alpha.15) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.13...@hocuspocus/extension-database@1.0.0-alpha.14) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.12...@hocuspocus/extension-database@1.0.0-alpha.13) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.11...@hocuspocus/extension-database@1.0.0-alpha.12) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.10...@hocuspocus/extension-database@1.0.0-alpha.11) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.9...@hocuspocus/extension-database@1.0.0-alpha.10) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.8...@hocuspocus/extension-database@1.0.0-alpha.9) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.7...@hocuspocus/extension-database@1.0.0-alpha.8) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.6...@hocuspocus/extension-database@1.0.0-alpha.7) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.5...@hocuspocus/extension-database@1.0.0-alpha.6) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.4...@hocuspocus/extension-database@1.0.0-alpha.5) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.3...@hocuspocus/extension-database@1.0.0-alpha.4) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.2...@hocuspocus/extension-database@1.0.0-alpha.3) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# [1.0.0-alpha.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-database@1.0.0-alpha.1...@hocuspocus/extension-database@1.0.0-alpha.2) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-database + + + + + +# 1.0.0-alpha.1 (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-database diff --git a/packages/extension-database/README.md b/packages/extension-database/README.md new file mode 100644 index 0000000..9a75173 --- /dev/null +++ b/packages/extension-database/README.md @@ -0,0 +1,51 @@ +# @hocuspocus/extension-database +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-database.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-database) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-database.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-database.svg)](https://www.npmjs.com/package/@hocuspocus/extension-database) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +A generic persistence base class for [Hocuspocus](https://github.com/ueberdosis/hocuspocus). Provide your own `fetch` and `store` functions to plug any database into the collaboration backend — Postgres, MySQL, MongoDB, a REST API, etc. + +## Installation + +```bash +npm install @hocuspocus/extension-database +``` + +## Usage + +Plug in any storage backend by implementing two async functions: + +```js +import { Server } from "@hocuspocus/server" +import { Database } from "@hocuspocus/extension-database" + +const server = new Server({ + extensions: [ + new Database({ + fetch: async ({ documentName }) => { + // return a Uint8Array of the stored Y.js update, or null for new documents + return await yourDb.findDocument(documentName) + }, + store: async ({ documentName, state }) => { + // persist the binary Y.js state wherever you like + await yourDb.upsertDocument(documentName, state) + }, + }), + ], +}) + +server.listen() +``` + +`state` is a `Buffer` containing the encoded Y.js update. Store it as-is and return it unchanged from `fetch`. + +For dedicated storage drivers, see [`@hocuspocus/extension-sqlite`](../extension-sqlite) or [`@hocuspocus/extension-s3`](../extension-s3), both of which extend this base class. + +## Documentation + +Persistence patterns and performance tips: [tiptap.dev/docs/hocuspocus/guides/persistence](https://tiptap.dev/docs/hocuspocus/guides/persistence). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). \ No newline at end of file diff --git a/packages/extension-database/package.json b/packages/extension-database/package.json new file mode 100644 index 0000000..cafe914 --- /dev/null +++ b/packages/extension-database/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hocuspocus/extension-database", + "description": "a generic Hocuspocus persistence driver for the database", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-database.cjs", + "module": "dist/hocuspocus-database.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-database.esm.js", + "require": "./dist/hocuspocus-database.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/server": "workspace:^" + }, + "peerDependencies": { + "yjs": "^13.6.8" + }, + "publishConfig": { + "access": "public" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + } +} diff --git a/packages/extension-database/src/Database.ts b/packages/extension-database/src/Database.ts new file mode 100644 index 0000000..dc85352 --- /dev/null +++ b/packages/extension-database/src/Database.ts @@ -0,0 +1,62 @@ +import type { + Extension, + fetchPayload, + onChangePayload, + onLoadDocumentPayload, + onStoreDocumentPayload, + storePayload, +} from "@hocuspocus/server"; +import * as Y from "yjs"; + +export interface DatabaseConfiguration { + /** + * Pass a Promise to retrieve updates from your database. The Promise should resolve to + * an array of items with Y.js-compatible binary data. + */ + fetch: (data: fetchPayload) => Promise; + /** + * Pass a function to store updates in your database. + */ + store: (data: storePayload) => Promise; +} + +export class Database implements Extension { + /** + * Default configuration + */ + configuration: DatabaseConfiguration = { + fetch: async () => null, + store: async () => {}, + }; + + /** + * Constructor + */ + constructor(configuration: Partial) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + } + + /** + * Get stored data from the database. + */ + async onLoadDocument(data: onLoadDocumentPayload): Promise { + const update = await this.configuration.fetch(data); + + if (update) { + Y.applyUpdate(data.document, update); + } + } + + /** + * Store new updates in the database. + */ + async onStoreDocument(data: onStoreDocumentPayload) { + await this.configuration.store({ + ...data, + state: Buffer.from(Y.encodeStateAsUpdate(data.document)), + }); + } +} diff --git a/packages/extension-database/src/index.ts b/packages/extension-database/src/index.ts new file mode 100644 index 0000000..cca6903 --- /dev/null +++ b/packages/extension-database/src/index.ts @@ -0,0 +1 @@ +export * from "./Database.ts"; diff --git a/packages/extension-logger/CHANGELOG.md b/packages/extension-logger/CHANGELOG.md new file mode 100644 index 0000000..58cb6e3 --- /dev/null +++ b/packages/extension-logger/CHANGELOG.md @@ -0,0 +1,1732 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.81](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.80...@hocuspocus/extension-logger@1.0.0-alpha.81) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.80](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.79...@hocuspocus/extension-logger@1.0.0-alpha.80) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.79](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.78...@hocuspocus/extension-logger@1.0.0-alpha.79) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.78](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.77...@hocuspocus/extension-logger@1.0.0-alpha.78) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.77](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.76...@hocuspocus/extension-logger@1.0.0-alpha.77) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.76](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.75...@hocuspocus/extension-logger@1.0.0-alpha.76) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.75](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.74...@hocuspocus/extension-logger@1.0.0-alpha.75) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.74](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.73...@hocuspocus/extension-logger@1.0.0-alpha.74) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.73](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.72...@hocuspocus/extension-logger@1.0.0-alpha.73) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.72](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.71...@hocuspocus/extension-logger@1.0.0-alpha.72) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.71](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.70...@hocuspocus/extension-logger@1.0.0-alpha.71) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.70](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.69...@hocuspocus/extension-logger@1.0.0-alpha.70) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.69](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.68...@hocuspocus/extension-logger@1.0.0-alpha.69) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.68](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.67...@hocuspocus/extension-logger@1.0.0-alpha.68) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.67](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.66...@hocuspocus/extension-logger@1.0.0-alpha.67) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.66](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.65...@hocuspocus/extension-logger@1.0.0-alpha.66) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.65](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.64...@hocuspocus/extension-logger@1.0.0-alpha.65) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.64](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.63...@hocuspocus/extension-logger@1.0.0-alpha.64) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.63](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.62...@hocuspocus/extension-logger@1.0.0-alpha.63) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.62](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.61...@hocuspocus/extension-logger@1.0.0-alpha.62) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.61](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.60...@hocuspocus/extension-logger@1.0.0-alpha.61) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.60](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.59...@hocuspocus/extension-logger@1.0.0-alpha.60) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.59](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.58...@hocuspocus/extension-logger@1.0.0-alpha.59) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.58](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.57...@hocuspocus/extension-logger@1.0.0-alpha.58) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.57](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.56...@hocuspocus/extension-logger@1.0.0-alpha.57) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.56](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.55...@hocuspocus/extension-logger@1.0.0-alpha.56) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.55](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.54...@hocuspocus/extension-logger@1.0.0-alpha.55) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.54](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.53...@hocuspocus/extension-logger@1.0.0-alpha.54) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.53](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.52...@hocuspocus/extension-logger@1.0.0-alpha.53) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.52](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.51...@hocuspocus/extension-logger@1.0.0-alpha.52) (2021-11-10) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.51](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.50...@hocuspocus/extension-logger@1.0.0-alpha.51) (2021-11-05) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.50](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.49...@hocuspocus/extension-logger@1.0.0-alpha.50) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.49](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.48...@hocuspocus/extension-logger@1.0.0-alpha.49) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.48](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.47...@hocuspocus/extension-logger@1.0.0-alpha.48) (2021-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.47](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.46...@hocuspocus/extension-logger@1.0.0-alpha.47) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.46](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.45...@hocuspocus/extension-logger@1.0.0-alpha.46) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.45](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.44...@hocuspocus/extension-logger@1.0.0-alpha.45) (2021-09-17) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.44](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.43...@hocuspocus/extension-logger@1.0.0-alpha.44) (2021-09-14) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.43](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.42...@hocuspocus/extension-logger@1.0.0-alpha.43) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.42](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.41...@hocuspocus/extension-logger@1.0.0-alpha.42) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.41](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.40...@hocuspocus/extension-logger@1.0.0-alpha.41) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.39...@hocuspocus/extension-logger@1.0.0-alpha.40) (2021-08-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.38...@hocuspocus/extension-logger@1.0.0-alpha.39) (2021-08-27) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.37...@hocuspocus/extension-logger@1.0.0-alpha.38) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.36...@hocuspocus/extension-logger@1.0.0-alpha.37) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.35...@hocuspocus/extension-logger@1.0.0-alpha.36) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.34...@hocuspocus/extension-logger@1.0.0-alpha.35) (2021-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.34](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.33...@hocuspocus/extension-logger@1.0.0-alpha.34) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.32...@hocuspocus/extension-logger@1.0.0-alpha.33) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.31...@hocuspocus/extension-logger@1.0.0-alpha.32) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.30...@hocuspocus/extension-logger@1.0.0-alpha.31) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.29...@hocuspocus/extension-logger@1.0.0-alpha.30) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.28...@hocuspocus/extension-logger@1.0.0-alpha.29) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.27...@hocuspocus/extension-logger@1.0.0-alpha.28) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.26...@hocuspocus/extension-logger@1.0.0-alpha.27) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.25...@hocuspocus/extension-logger@1.0.0-alpha.26) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.24...@hocuspocus/extension-logger@1.0.0-alpha.25) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.23...@hocuspocus/extension-logger@1.0.0-alpha.24) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.22...@hocuspocus/extension-logger@1.0.0-alpha.23) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.21...@hocuspocus/extension-logger@1.0.0-alpha.22) (2021-04-15) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.20...@hocuspocus/extension-logger@1.0.0-alpha.21) (2021-04-14) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.19...@hocuspocus/extension-logger@1.0.0-alpha.20) (2021-04-12) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.18...@hocuspocus/extension-logger@1.0.0-alpha.19) (2021-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.17...@hocuspocus/extension-logger@1.0.0-alpha.18) (2021-04-07) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.16...@hocuspocus/extension-logger@1.0.0-alpha.17) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.15...@hocuspocus/extension-logger@1.0.0-alpha.16) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.14...@hocuspocus/extension-logger@1.0.0-alpha.15) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-logger@1.0.0-alpha.13...@hocuspocus/extension-logger@1.0.0-alpha.14) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# 1.0.0-alpha.13 (2021-03-25) + +**Note:** Version bump only for package @hocuspocus/extension-logger + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.11...@hocuspocus/logger@1.0.0-alpha.12) (2021-03-18) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.10...@hocuspocus/logger@1.0.0-alpha.11) (2021-03-15) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.9...@hocuspocus/logger@1.0.0-alpha.10) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.8...@hocuspocus/logger@1.0.0-alpha.9) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.7...@hocuspocus/logger@1.0.0-alpha.8) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.6...@hocuspocus/logger@1.0.0-alpha.7) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.5...@hocuspocus/logger@1.0.0-alpha.6) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.4...@hocuspocus/logger@1.0.0-alpha.5) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.3...@hocuspocus/logger@1.0.0-alpha.4) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/logger@1.0.0-alpha.2...@hocuspocus/logger@1.0.0-alpha.3) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger + + + + + +# 1.0.0-alpha.2 (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/logger diff --git a/packages/extension-logger/README.md b/packages/extension-logger/README.md new file mode 100644 index 0000000..4519c7c --- /dev/null +++ b/packages/extension-logger/README.md @@ -0,0 +1,56 @@ +# @hocuspocus/extension-logger +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-logger.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-logger) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-logger.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-logger.svg)](https://www.npmjs.com/package/@hocuspocus/extension-logger) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +Logs connection, document, and lifecycle events for a [Hocuspocus](https://github.com/ueberdosis/hocuspocus) server to the console. Useful during development and debugging to see when clients connect, documents load and store, and changes come in. + +## Installation + +```bash +npm install @hocuspocus/extension-logger +``` + +## Usage + +```js +import { Server } from "@hocuspocus/server" +import { Logger } from "@hocuspocus/extension-logger" + +const server = new Server({ + extensions: [new Logger()], +}) + +server.listen() +``` + +### Silence specific hooks + +Pass booleans per hook to quiet down the ones you don't care about: + +```js +new Logger({ + onConnect: false, + onDisconnect: false, + onChange: false, +}) +``` + +### Use a custom log function + +Route output to your own logger (pino, winston, structured JSON, etc.): + +```js +new Logger({ + log: (...args) => myLogger.info({ msg: args.join(" ") }), +}) +``` + +## Documentation + +Full options reference: [tiptap.dev/docs/hocuspocus/server/extensions/logger](https://tiptap.dev/docs/hocuspocus/server/extensions/logger). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/extension-logger/package.json b/packages/extension-logger/package.json new file mode 100644 index 0000000..dec1112 --- /dev/null +++ b/packages/extension-logger/package.json @@ -0,0 +1,41 @@ +{ + "name": "@hocuspocus/extension-logger", + "version": "4.7.0", + "description": "hocuspocus logging extension", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "logging", + "logger", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-logger.cjs", + "module": "dist/hocuspocus-logger.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-logger.esm.js", + "require": "./dist/hocuspocus-logger.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/server": "workspace:^" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/extension-logger/src/Logger.ts b/packages/extension-logger/src/Logger.ts new file mode 100644 index 0000000..d960fc0 --- /dev/null +++ b/packages/extension-logger/src/Logger.ts @@ -0,0 +1,164 @@ +import type { + Extension, + onChangePayload, + onConfigurePayload, + onConnectPayload, + onDestroyPayload, + onDisconnectPayload, + onLoadDocumentPayload, + onRequestPayload, + onStoreDocumentPayload, + onUpgradePayload, +} from "@hocuspocus/server"; + +export interface LoggerConfiguration { + /** + * Prepend all logging message with a string. + * + * @deprecated + */ + prefix: null | string; + /** + * Whether to log something for the `onLoadDocument` hook. + */ + onLoadDocument: boolean; + /** + * Whether to log something for the `onChange` hook. + */ + onChange: boolean; + /** + * Whether to log something for the `onStoreDocument` hook. + */ + onStoreDocument: boolean; + /** + * Whether to log something for the `onConnect` hook. + */ + onConnect: boolean; + /** + * Whether to log something for the `onDisconnect` hook. + */ + onDisconnect: boolean; + /** + * Whether to log something for the `onUpgrade` hook. + */ + onUpgrade: boolean; + /** + * Whether to log something for the `onRequest` hook. + */ + onRequest: boolean; + /** + * Whether to log something for the `onDestroy` hook. + */ + onDestroy: boolean; + /** + * Whether to log something for the `onConfigure` hook. + */ + onConfigure: boolean; + /** + * A log function, if none is provided output will go to console + */ + log: (...args: any[]) => void; +} + +export class Logger implements Extension { + name: string | null = null; + + configuration: LoggerConfiguration = { + prefix: null, + onLoadDocument: true, + onChange: true, + onStoreDocument: true, + onConnect: true, + onDisconnect: true, + onUpgrade: true, + onRequest: true, + onDestroy: true, + onConfigure: true, + log: console.log, // eslint-disable-line + }; + + /** + * Constructor + */ + constructor(configuration?: Partial) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + } + + async onConfigure(data: onConfigurePayload) { + this.name = data.instance.configuration.name; + + if (!this.configuration.onConfigure) { + return; + } + + if (this.configuration.prefix) { + console.warn( + "[hocuspocus warn] The Logger 'prefix' is deprecated. Pass a 'name' to the Hocuspocus configuration instead.", + ); + } + } + + async onLoadDocument(data: onLoadDocumentPayload) { + if (this.configuration.onLoadDocument) { + this.log(`Loaded document "${data.documentName}".`); + } + } + + async onChange(data: onChangePayload) { + if (this.configuration.onChange) { + this.log(`Document "${data.documentName}" changed.`); + } + } + + async onStoreDocument(data: onStoreDocumentPayload) { + if (this.configuration.onStoreDocument) { + this.log(`Store "${data.documentName}".`); + } + } + + async onConnect(data: onConnectPayload) { + if (this.configuration.onConnect) { + this.log(`New connection to "${data.documentName}".`); + } + } + + async onDisconnect(data: onDisconnectPayload) { + if (this.configuration.onDisconnect) { + this.log(`Connection to "${data.documentName}" closed.`); + } + } + + async onUpgrade(data: onUpgradePayload) { + if (this.configuration.onUpgrade) { + this.log("Upgrading connection …"); + } + } + + async onRequest(data: onRequestPayload) { + if (this.configuration.onRequest) { + this.log(`Incoming HTTP Request to ${data.request.url}`); + } + } + + async onDestroy(data: onDestroyPayload) { + if (this.configuration.onDestroy) { + this.log("Shut down."); + } + } + + private log(message: string) { + const date = new Date().toISOString(); + let meta = `${date}`; + + if (this.name) { + meta = `${this.name} ${meta}`; + } + + message = `[${meta}] ${message}`; + + this.configuration.log(message); + } +} diff --git a/packages/extension-logger/src/index.ts b/packages/extension-logger/src/index.ts new file mode 100644 index 0000000..9a59b0a --- /dev/null +++ b/packages/extension-logger/src/index.ts @@ -0,0 +1 @@ +export * from "./Logger.ts"; diff --git a/packages/extension-redis/CHANGELOG.md b/packages/extension-redis/CHANGELOG.md new file mode 100644 index 0000000..786178b --- /dev/null +++ b/packages/extension-redis/CHANGELOG.md @@ -0,0 +1,1683 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + + +### Bug Fixes + +* **extension-redis:** publish sync replies to the requester only ([#1152](https://github.com/ueberdosis/hocuspocus/issues/1152)) ([7fb9086](https://github.com/ueberdosis/hocuspocus/commit/7fb9086597a12a9220826330eb83426e081f7eea)), closes [#1151](https://github.com/ueberdosis/hocuspocus/issues/1151) + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + + +### Features + +* add consistent state synchronization across Redis instances ([#1119](https://github.com/ueberdosis/hocuspocus/issues/1119)) ([0051a6c](https://github.com/ueberdosis/hocuspocus/commit/0051a6cb7618290d1f574da7ad61da2be77f839d)) + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Bug Fixes + +* remove unused packages ([#1090](https://github.com/ueberdosis/hocuspocus/issues/1090)) ([66fb35c](https://github.com/ueberdosis/hocuspocus/commit/66fb35cff71d986d671fe9a8118ce4da87507b44)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + + +### Features + +* optimize retry logic ([302eb08](https://github.com/ueberdosis/hocuspocus/commit/302eb08d9a2a2b59d44424571fdc70d5790d8fd8)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + + +### Bug Fixes + +* on awareness craah prevention and memory leak onStoreDocument ([#1032](https://github.com/ueberdosis/hocuspocus/issues/1032)) ([67957ae](https://github.com/ueberdosis/hocuspocus/commit/67957ae4d114c07b8b63cf2900a7ae07f1300f72)), closes [#1](https://github.com/ueberdosis/hocuspocus/issues/1) [#3](https://github.com/ueberdosis/hocuspocus/issues/3) [#1027](https://github.com/ueberdosis/hocuspocus/issues/1027) + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + + +### Features + +* rewrite of redis unloading logic to utilize before/after-unloadDocument ([#1007](https://github.com/ueberdosis/hocuspocus/issues/1007)) ([8f86caf](https://github.com/ueberdosis/hocuspocus/commit/8f86caff4e74601ecc98fff293fc58584af27176)) + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Bug Fixes + +* **extension-redis:** avoid hard crashes of expected errors when acquiring a lock ([#983](https://github.com/ueberdosis/hocuspocus/issues/983)) ([38e58a4](https://github.com/ueberdosis/hocuspocus/commit/38e58a4d099cf0f5fba0f51cbd7be169de94b0bd)) + + +### Features + +* remove uuid dependency and replace usages by crypto.randomUUID ([#1005](https://github.com/ueberdosis/hocuspocus/issues/1005)) ([393189f](https://github.com/ueberdosis/hocuspocus/commit/393189fe448c68cbd741642866be4b4b23546f17)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + + +### Bug Fixes + +* **extension-redis:** redlock release race condition ([#958](https://github.com/ueberdosis/hocuspocus/issues/958)) ([5666a65](https://github.com/ueberdosis/hocuspocus/commit/5666a656f9bd2419fb8565c99a812698d1ff9dcf)) + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + + +### Bug Fixes + +* **extension-redis:** unsubscribe from pubsub ([#945](https://github.com/ueberdosis/hocuspocus/issues/945)) ([0812d48](https://github.com/ueberdosis/hocuspocus/commit/0812d4810c80ae6118c9376cced1d03c8f1f95dc)) + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Features + +* extension-redis should watch the ydoc for changes and notify other hocuspocus instances about it (via redis pub/sub) ([bacc535](https://github.com/ueberdosis/hocuspocus/commit/bacc53594550aa771765f8e123c9797a88071a20)) + + + + + +# [1.0.0-alpha.70](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.69...@hocuspocus/extension-redis@1.0.0-alpha.70) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.69](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.68...@hocuspocus/extension-redis@1.0.0-alpha.69) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.68](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.67...@hocuspocus/extension-redis@1.0.0-alpha.68) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.67](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.66...@hocuspocus/extension-redis@1.0.0-alpha.67) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.66](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.65...@hocuspocus/extension-redis@1.0.0-alpha.66) (2022-06-08) + + +### Features + +* extension-redis should watch the ydoc for changes and notify other hocuspocus instances about it (via redis pub/sub) ([bacc535](https://github.com/ueberdosis/hocuspocus/commit/bacc53594550aa771765f8e123c9797a88071a20)) + + + + + +# [1.0.0-alpha.65](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.64...@hocuspocus/extension-redis@1.0.0-alpha.65) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.64](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.63...@hocuspocus/extension-redis@1.0.0-alpha.64) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.63](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.62...@hocuspocus/extension-redis@1.0.0-alpha.63) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.62](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.61...@hocuspocus/extension-redis@1.0.0-alpha.62) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.61](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.60...@hocuspocus/extension-redis@1.0.0-alpha.61) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.60](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.59...@hocuspocus/extension-redis@1.0.0-alpha.60) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.59](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.58...@hocuspocus/extension-redis@1.0.0-alpha.59) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.58](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.57...@hocuspocus/extension-redis@1.0.0-alpha.58) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.57](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.56...@hocuspocus/extension-redis@1.0.0-alpha.57) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.56](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.55...@hocuspocus/extension-redis@1.0.0-alpha.56) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.55](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.54...@hocuspocus/extension-redis@1.0.0-alpha.55) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.54](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.53...@hocuspocus/extension-redis@1.0.0-alpha.54) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.53](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.52...@hocuspocus/extension-redis@1.0.0-alpha.53) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.52](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.51...@hocuspocus/extension-redis@1.0.0-alpha.52) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.51](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.50...@hocuspocus/extension-redis@1.0.0-alpha.51) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.50](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.49...@hocuspocus/extension-redis@1.0.0-alpha.50) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.49](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.48...@hocuspocus/extension-redis@1.0.0-alpha.49) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.48](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.47...@hocuspocus/extension-redis@1.0.0-alpha.48) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.47](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.46...@hocuspocus/extension-redis@1.0.0-alpha.47) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.46](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.45...@hocuspocus/extension-redis@1.0.0-alpha.46) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.45](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.44...@hocuspocus/extension-redis@1.0.0-alpha.45) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.44](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.43...@hocuspocus/extension-redis@1.0.0-alpha.44) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.43](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.42...@hocuspocus/extension-redis@1.0.0-alpha.43) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.42](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.41...@hocuspocus/extension-redis@1.0.0-alpha.42) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.41](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.40...@hocuspocus/extension-redis@1.0.0-alpha.41) (2021-11-10) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.39...@hocuspocus/extension-redis@1.0.0-alpha.40) (2021-11-05) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.38...@hocuspocus/extension-redis@1.0.0-alpha.39) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.37...@hocuspocus/extension-redis@1.0.0-alpha.38) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.36...@hocuspocus/extension-redis@1.0.0-alpha.37) (2021-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.35...@hocuspocus/extension-redis@1.0.0-alpha.36) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.35) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.33) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.32) (2021-09-17) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.31) (2021-09-14) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.30) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.28...@hocuspocus/extension-redis@1.0.0-alpha.29) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.27...@hocuspocus/extension-redis@1.0.0-alpha.28) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.26...@hocuspocus/extension-redis@1.0.0-alpha.27) (2021-08-29) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.25...@hocuspocus/extension-redis@1.0.0-alpha.26) (2021-08-27) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.24...@hocuspocus/extension-redis@1.0.0-alpha.25) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.23...@hocuspocus/extension-redis@1.0.0-alpha.24) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.22...@hocuspocus/extension-redis@1.0.0-alpha.23) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.21...@hocuspocus/extension-redis@1.0.0-alpha.22) (2021-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.20...@hocuspocus/extension-redis@1.0.0-alpha.21) (2021-06-23) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.19...@hocuspocus/extension-redis@1.0.0-alpha.20) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.18...@hocuspocus/extension-redis@1.0.0-alpha.19) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.17...@hocuspocus/extension-redis@1.0.0-alpha.18) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.16...@hocuspocus/extension-redis@1.0.0-alpha.17) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.15...@hocuspocus/extension-redis@1.0.0-alpha.16) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.14...@hocuspocus/extension-redis@1.0.0-alpha.15) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.13...@hocuspocus/extension-redis@1.0.0-alpha.14) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.12...@hocuspocus/extension-redis@1.0.0-alpha.13) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.11...@hocuspocus/extension-redis@1.0.0-alpha.12) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.10...@hocuspocus/extension-redis@1.0.0-alpha.11) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.9...@hocuspocus/extension-redis@1.0.0-alpha.10) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.8...@hocuspocus/extension-redis@1.0.0-alpha.9) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.7...@hocuspocus/extension-redis@1.0.0-alpha.8) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.6...@hocuspocus/extension-redis@1.0.0-alpha.7) (2021-04-15) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.5...@hocuspocus/extension-redis@1.0.0-alpha.6) (2021-04-14) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.4...@hocuspocus/extension-redis@1.0.0-alpha.5) (2021-04-12) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.3...@hocuspocus/extension-redis@1.0.0-alpha.4) (2021-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.2...@hocuspocus/extension-redis@1.0.0-alpha.3) (2021-04-07) + +**Note:** Version bump only for package @hocuspocus/extension-redis + + + + + +# [1.0.0-alpha.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-redis@1.0.0-alpha.33...@hocuspocus/extension-redis@1.0.0-alpha.2) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/extension-redis diff --git a/packages/extension-redis/README.md b/packages/extension-redis/README.md new file mode 100644 index 0000000..95e949c --- /dev/null +++ b/packages/extension-redis/README.md @@ -0,0 +1,76 @@ +# @hocuspocus/extension-redis +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-redis.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-redis) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-redis.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-redis.svg)](https://www.npmjs.com/package/@hocuspocus/extension-redis) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +Scale [Hocuspocus](https://github.com/ueberdosis/hocuspocus) horizontally across multiple server instances. Uses Redis pub/sub to broadcast document updates and awareness between servers, so clients connected to different instances stay in sync on the same document. + +## Installation + +```bash +npm install @hocuspocus/extension-redis +``` + +## Usage + +Point every Hocuspocus instance at the same Redis: + +```js +import { Server } from "@hocuspocus/server" +import { Redis } from "@hocuspocus/extension-redis" + +const server = new Server({ + port: 1234, + extensions: [ + new Redis({ + host: "127.0.0.1", + port: 6379, + }), + ], +}) + +server.listen() +``` + +### Bring your own Redis client + +If you already have an `ioredis` client (including Cluster), pass it in directly instead of host/port: + +```js +import RedisClient from "ioredis" + +new Redis({ + redis: new RedisClient({ host: "127.0.0.1", port: 6379 }), +}) +``` + +Redis handles real-time sync — you still need a persistence extension (e.g. [`@hocuspocus/extension-sqlite`](../extension-sqlite), [`@hocuspocus/extension-s3`](../extension-s3), or your own [`Database`](../extension-database)) to store documents long-term. + +### Consistent reads on load + +When a document is loaded on one instance while another instance already has it +open (and possibly modified in memory, not yet persisted), the load blocks until +the other instance has sent its current state — so a `DirectConnection` or a +freshly connected client observes the latest collaborative content rather than +just what was read from storage. If no other instance has the document open, the +load is not delayed. + +The wait is bounded by `awaitInitialSyncTimeout` (ms, default `1000`). Set it to +`0` to disable the wait and restore the previous fire-and-forget behavior: + +```js +new Redis({ + host: "127.0.0.1", + port: 6379, + awaitInitialSyncTimeout: 1000, +}) +``` + +## Documentation + +Full scaling architecture and multi-instance deployment guide: [tiptap.dev/docs/hocuspocus/server/extensions/redis](https://tiptap.dev/docs/hocuspocus/server/extensions/redis). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/extension-redis/package.json b/packages/extension-redis/package.json new file mode 100644 index 0000000..2dedf2f --- /dev/null +++ b/packages/extension-redis/package.json @@ -0,0 +1,48 @@ +{ + "name": "@hocuspocus/extension-redis", + "version": "4.7.0", + "description": "Scale Hocuspocus horizontally with Redis", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "redis", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-redis.cjs", + "module": "dist/hocuspocus-redis.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-redis.esm.js", + "require": "./dist/hocuspocus-redis.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/common": "workspace:^", + "@hocuspocus/server": "workspace:^", + "@sesamecare-oss/redlock": "^1.4.0", + "ioredis": "~5.6.1", + "kleur": "^4.1.5" + }, + "peerDependencies": { + "y-protocols": "^1.0.6", + "yjs": "^13.6.8" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/extension-redis/src/Redis.ts b/packages/extension-redis/src/Redis.ts new file mode 100644 index 0000000..0b60574 --- /dev/null +++ b/packages/extension-redis/src/Redis.ts @@ -0,0 +1,839 @@ +import crypto from "node:crypto"; +import { SkipFurtherHooksError } from "@hocuspocus/common"; +import type { + afterLoadDocumentPayload, + afterStoreDocumentPayload, + afterUnloadDocumentPayload, + beforeBroadcastStatelessPayload, + beforeUnloadDocumentPayload, + Document, + Extension, + Hocuspocus, + onAwarenessUpdatePayload, + onChangePayload, + onConfigurePayload, + onStoreDocumentPayload, + RedisTransactionOrigin, +} from "@hocuspocus/server"; +import { + IncomingMessage, + isTransactionOrigin, + MessageReceiver, + MessageType, + OutgoingMessage, +} from "@hocuspocus/server"; +import { + ExecutionError, + type ExecutionResult, + type Lock, + Redlock, +} from "@sesamecare-oss/redlock"; +import type { + Cluster, + ClusterNode, + ClusterOptions, + RedisOptions, +} from "ioredis"; +import RedisClient from "ioredis"; +import type { Awareness } from "y-protocols/awareness"; +import { messageYjsSyncStep2, messageYjsUpdate } from "y-protocols/sync"; +export type RedisInstance = RedisClient | Cluster; +export interface Configuration { + /** + * Redis port + */ + port: number; + /** + * Redis host + */ + host: string; + /** + * Redis Cluster + */ + nodes?: ClusterNode[]; + /** + * Duplicate from an existed Redis instance + */ + redis?: RedisInstance; + /** + * Redis instance creator + */ + createClient?: () => RedisInstance; + /** + * Options passed directly to Redis constructor + * + * https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options + */ + options?: ClusterOptions | RedisOptions; + /** + * An unique instance name, required to filter messages in Redis. + * If none is provided an unique id is generated. + */ + identifier: string; + /** + * Namespace for Redis keys, if none is provided 'hocuspocus' is used + */ + prefix: string; + /** + * The maximum time for the Redis lock in ms (in case it can’t be released). + */ + lockTimeout: number; + /** + * A delay before onDisconnect is executed. This allows last minute updates' + * sync messages to be received by the subscription before it's closed. + */ + disconnectDelay: number; + /** + * The maximum time (in ms) `afterLoadDocument` waits for another instance to + * send its state when loading a document that is already open elsewhere. + * + * When a document is loaded on this instance while another instance already + * has it open (and possibly modified in memory, not yet persisted), we + * publish a SyncStep1 and block the load until the peer replies with its + * state (SyncStep2) — or this timeout elapses. This guarantees that callers + * (most importantly a `DirectConnection`) observe the latest collaborative + * state instead of just what was loaded from storage. + * + * If no other instance is subscribed to the document, the wait is skipped + * entirely, so a cold/lone load is not delayed. + * + * Set to `0` to disable the wait and restore the legacy fire-and-forget + * behavior. + */ + awaitInitialSyncTimeout: number; +} + +/** + * Publishes for one document, accumulated over a single event loop turn. + * + * The two kinds are independent, not alternatives: a turn can carry a sync step, + * an awareness update, or both, and each is sent on its own. + */ +type PendingPublish = { + document: Document; + + /** A SyncStep1 is pending. A flag, not a count: state vectors are idempotent. */ + syncStepPending: boolean; + + /** Clients whose awareness changed this turn; empty when none did. */ + awarenessClients: Set; + + /** Set alongside `awarenessClients`; the awareness they belong to. */ + awareness?: Awareness; + + cancel: () => void; +}; + +export class Redis implements Extension { + /** + * Make sure to give that extension a higher priority, so + * the `onStoreDocument` hook is able to intercept the chain, + * before documents are stored to the database. + */ + priority = 1000; + + configuration: Configuration = { + port: 6379, + host: "127.0.0.1", + prefix: "hocuspocus", + identifier: `host-${crypto.randomUUID()}`, + lockTimeout: 1000, + disconnectDelay: 1000, + awaitInitialSyncTimeout: 1000, + }; + + redisTransactionOrigin: RedisTransactionOrigin = { + source: "redis", + }; + + pub: RedisInstance; + + sub: RedisInstance; + + instance!: Hocuspocus; + + redlock: Redlock; + + locks = new Map }>(); + + messagePrefix: Buffer; + + /** + * Resolves once this instance is subscribed to its own reply channel. + * Awaited before a document announces itself, so a peer's reply can never + * arrive on a channel we are not listening to yet. + */ + private replySubscription: Promise; + + private pendingAfterStoreDocumentResolves = new Map< + string, + { timeout: NodeJS.Timeout; resolve: () => void } + >(); + + /** + * Documents this instance has loaded and subscribed to, keyed by name. + * + * This mirrors `instance.documents` but is populated at the very start of + * `afterLoadDocument` — i.e. *before* Hocuspocus registers the document in + * `instance.documents` (which only happens once loading, including this + * hook, has fully resolved). That early registration is what lets + * `handleIncomingMessage` apply a peer's SyncStep2 reply while we are still + * blocking the load on it. + */ + private documents = new Map(); + + /** + * Resolvers for in-flight `afterLoadDocument` waits, keyed by document name. + * Invoked by `handleIncomingMessage` as soon as a peer's state arrives. + */ + private pendingInitialSyncResolves = new Map void>(); + + /** + * Publishes coalesced into the current event loop turn, keyed by document + * name. See `schedulePublish`. + */ + private pendingPublishes = new Map(); + + public constructor(configuration: Partial) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + + // Create Redis instance + const { port, host, options, nodes, redis, createClient } = + this.configuration; + + if (typeof createClient === "function") { + this.pub = createClient(); + this.sub = createClient(); + } else if (redis) { + this.pub = redis.duplicate(); + this.sub = redis.duplicate(); + } else if (nodes && nodes.length > 0) { + this.pub = new RedisClient.Cluster(nodes, options); + this.sub = new RedisClient.Cluster(nodes, options); + } else { + this.pub = new RedisClient(port, host, options ?? {}); + this.sub = new RedisClient(port, host, options ?? {}); + } + + this.sub.on("messageBuffer", this.handleIncomingMessage); + + this.redlock = new Redlock([this.pub], { + retryCount: 0, + }); + + const identifierBuffer = Buffer.from( + this.configuration.identifier, + "utf-8", + ); + this.messagePrefix = Buffer.concat([ + Buffer.from([identifierBuffer.length]), + identifierBuffer, + ]); + + // Subscribed once for the lifetime of the extension rather than per + // document: `handleIncomingMessage` resolves the document from the message + // payload, so a single channel per instance receives every reply addressed + // to us. Started here instead of in `onConfigure` because the server does + // not await that hook; `afterLoadDocument` awaits this promise, so no + // reply can be missed while the subscription is still pending. + this.replySubscription = new Promise((resolve, reject) => { + this.sub.subscribe( + this.replyKey(this.configuration.identifier), + (error: any) => { + if (error) { + reject(error); + return; + } + + resolve(); + }, + ); + }); + + // The error is re-thrown to whoever awaits the promise in + // `afterLoadDocument`; this handler only keeps it from being reported as + // an unhandled rejection when no document is ever loaded. + this.replySubscription.catch(() => {}); + } + + async onConfigure({ instance }: onConfigurePayload) { + this.instance = instance; + } + + private getKey(documentName: string) { + return `${this.configuration.prefix}:${documentName}`; + } + + private pubKey(documentName: string) { + return this.getKey(documentName); + } + + private subKey(documentName: string) { + return this.getKey(documentName); + } + + private lockKey(documentName: string) { + return `${this.getKey(documentName)}:lock`; + } + + /** + * The channel an instance receives replies to its own requests on. + * + * Separated from the prefix by `#` rather than `:`, so it can never collide + * with the document channel of a document that happens to be named + * `reply:`. + */ + private replyKey(identifier: string) { + return `${this.configuration.prefix}#reply:${identifier}`; + } + + private encodeMessage(message: Uint8Array) { + return Buffer.concat([this.messagePrefix, Buffer.from(message)]); + } + + private decodeMessage(buffer: Buffer): [identifier: string, message: Buffer] { + const identifierLength = buffer[0]; + const identifier = buffer.toString("utf-8", 1, identifierLength + 1); + + return [identifier, buffer.slice(identifierLength + 1)]; + } + + /** + * Once a document is loaded, subscribe to the channel in Redis. + */ + public async afterLoadDocument({ + documentName, + document, + }: afterLoadDocumentPayload) { + // Track the document locally right away so `handleIncomingMessage` can + // apply inbound messages to it even before Hocuspocus has finished + // loading it (it only lands in `instance.documents` after this hook + // resolves). Without this, the SyncStep2 reply we request below would be + // dropped while we wait for it. + this.documents.set(documentName, document); + + try { + // Replies to the SyncStep1 we are about to publish are addressed to + // this instance's reply channel, so that subscription has to be live + // first. + await this.replySubscription; + + await new Promise((resolve, reject) => { + // On document creation the node will connect to pub and sub channels + // for the document. + this.sub.subscribe(this.subKey(documentName), (error: any) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); + + await this.syncInitialStateFromPeers(documentName, document); + } catch (error) { + // Loading failed: stop tracking the document so future messages aren't + // applied to a doc that never finished loading, release any pending + // wait, and best-effort unsubscribe (afterUnloadDocument does not run + // for a load that never completed). + this.documents.delete(documentName); + this.pendingInitialSyncResolves.delete(documentName); + this.sub.unsubscribe(this.subKey(documentName), () => {}); + throw error; + } + } + + /** + * Announce our state to other instances and — when another instance already + * has this document open — block until it has sent us its state (or the + * configured timeout elapses). See {@link Configuration.awaitInitialSyncTimeout}. + */ + private async syncInitialStateFromPeers( + documentName: string, + document: Document, + ) { + const timeout = this.configuration.awaitInitialSyncTimeout; + + const waitForPeers = + timeout > 0 && (await this.hasOtherSubscribers(documentName)); + + // Announce our state and request awareness. Awaited so a Redis publish + // failure surfaces as a load error instead of an unhandled rejection. + await Promise.all([ + this.publishFirstSyncStep(documentName, document), + this.requestAwarenessFromOtherInstances(documentName), + ]); + + // Skip the wait when it's disabled or nobody else has the document open: + // there is no peer to sync from, so blocking would only add latency. + if (!waitForPeers) { + return; + } + + // A peer has the document open. Block until it replies with its state + // (a SyncStep2/Update applied via handleIncomingMessage resolves this) or + // the timeout elapses. The reply cannot arrive before the SyncStep1 we + // just published is delivered, so registering the resolver now — right + // after the awaited publish, before yielding to the event loop — cannot + // miss it. + await new Promise((resolve) => { + const timer = setTimeout(() => { + this.pendingInitialSyncResolves.delete(documentName); + resolve(); + }, timeout); + + this.pendingInitialSyncResolves.set(documentName, () => { + clearTimeout(timer); + this.pendingInitialSyncResolves.delete(documentName); + resolve(); + }); + }); + } + + /** + * Whether another instance is currently subscribed to a document's channel. + * We are subscribed ourselves, so a subscriber count greater than one means + * at least one peer has the document open. + */ + private async hasOtherSubscribers(documentName: string): Promise { + try { + const result = (await this.pub.pubsub( + "NUMSUB", + this.subKey(documentName), + )) as [string, number | string]; + return Number(result?.[1] ?? 0) > 1; + } catch { + // NUMSUB may be unavailable in some setups (e.g. certain clusters). + // Assume peers might exist so correctness is preserved; the timeout + // still bounds the wait. + return true; + } + } + + /** + * Resolve a pending `afterLoadDocument` wait once a peer's state has been + * applied. + */ + private resolveInitialSync(documentName: string) { + this.pendingInitialSyncResolves.get(documentName)?.(); + } + + /** + * Peek whether a message carries another instance's document state + * (SyncStep2 or Update) without consuming the decoder. A SyncStep1 only + * *requests* state, so it must not release an initial-sync wait. + * + * Carrying state is necessary but not sufficient to release that wait: the + * state also has to have been computed for *our* state vector, which is what + * arriving on our reply channel means. See `handleIncomingMessage`. + */ + private messageCarriesPeerState(message: IncomingMessage): boolean { + const { pos } = message.decoder; + try { + const type = message.readVarUint(); + if (type !== MessageType.Sync && type !== MessageType.SyncReply) { + return false; + } + const syncType = message.readVarUint(); + return syncType === messageYjsSyncStep2 || syncType === messageYjsUpdate; + } catch { + return false; + } finally { + message.decoder.pos = pos; + } + } + + /** + * Coalesce this document's Redis publishes into the current event loop turn. + * + * Both publishers are per-change and neither is covered by the server's + * `flushDelay` batching: `onChange` runs synchronously by design (it also + * drives the store hooks) and `onAwarenessUpdate` is dispatched from a raw + * awareness listener rather than from `Document`'s batched broadcast. Left + * uncoalesced, a document with many writing clients publishes one SyncStep1 + * per change and every peer answers each one with a full + * `encodeStateAsUpdate` diff — which is O(document), so this costs far more + * per change than the WebSocket fan-out ever did. + * + * Collapsing is lossless. A SyncStep1 carries the document's *current* state + * vector, so the last one of a turn is a superset of the ones it replaces + * and a peer's reply to it covers every skipped change. Awareness collapses + * to the latest state of each changed client, exactly as `Document` does for + * its own fan-out. + */ + private schedulePublish( + documentName: string, + document: Document, + ): PendingPublish { + const existing = this.pendingPublishes.get(documentName); + + if (existing) { + if (existing.document === document) { + return existing; + } + + // The document was reloaded under the same name. `afterUnloadDocument` + // returns early on a fast reconnect, so it cannot be relied on to have + // cleared this. Whatever is buffered describes the previous instance — + // its state vector and its awareness — so drop it rather than publish + // it on behalf of the new one. + existing.cancel(); + this.pendingPublishes.delete(documentName); + } + + const handle = setImmediate(() => { + this.flushPublish(documentName).catch((error) => { + console.error( + `Redis: failed to publish for document "${documentName}"`, + error, + ); + }); + }); + + const pending: PendingPublish = { + document, + syncStepPending: false, + awarenessClients: new Set(), + cancel: () => clearImmediate(handle), + }; + + this.pendingPublishes.set(documentName, pending); + + return pending; + } + + private cancelPendingPublish(documentName: string) { + const pending = this.pendingPublishes.get(documentName); + + if (pending) { + pending.cancel(); + this.pendingPublishes.delete(documentName); + } + } + + /** + * Send everything accumulated for this document in the current turn. + */ + private async flushPublish(documentName: string) { + const pending = this.pendingPublishes.get(documentName); + + if (!pending) { + return; + } + + this.pendingPublishes.delete(documentName); + + // Both are issued before anything is awaited, so a failing sync step + // cannot swallow the awareness update — the entry is already dropped from + // the map at this point, and those clients would never be retried. + const publishes: Array> = []; + + if (pending.syncStepPending) { + publishes.push(this.publishFirstSyncStep(documentName, pending.document)); + } + + // Same guard as the uncoalesced path: publishing awareness for a document + // nobody is connected to throws (see hocuspocus#1027). + if ( + pending.awarenessClients.size > 0 && + pending.awareness && + pending.document.connections.size > 0 + ) { + const message = new OutgoingMessage( + documentName, + ).createAwarenessUpdateMessage( + pending.awareness, + Array.from(pending.awarenessClients), + ); + + publishes.push( + this.pub.publish( + this.pubKey(documentName), + this.encodeMessage(message.toUint8Array()), + ), + ); + } + + await Promise.all(publishes); + } + + /** + * Publish the first sync step through Redis. + */ + private async publishFirstSyncStep(documentName: string, document: Document) { + const syncMessage = new OutgoingMessage(documentName) + .createSyncMessage() + .writeFirstSyncStepFor(document); + + return this.pub.publish( + this.pubKey(documentName), + this.encodeMessage(syncMessage.toUint8Array()), + ); + } + + /** + * Let’s ask Redis who is connected already. + */ + private async requestAwarenessFromOtherInstances(documentName: string) { + const awarenessMessage = new OutgoingMessage( + documentName, + ).writeQueryAwareness(); + + return this.pub.publish( + this.pubKey(documentName), + this.encodeMessage(awarenessMessage.toUint8Array()), + ); + } + + /** + * Before the document is stored, make sure to set a lock in Redis. + * That’s meant to avoid conflicts with other instances trying to store the document. + */ + async onStoreDocument({ documentName }: onStoreDocumentPayload) { + // Attempt to acquire a lock and read lastReceivedTimestamp from Redis, + // to avoid conflict with other instances storing the same document. + const resource = this.lockKey(documentName); + const ttl = this.configuration.lockTimeout; + try { + const lock = await this.redlock.acquire([resource], ttl); + const oldLock = this.locks.get(resource); + if (oldLock?.release) { + await oldLock.release; + } + this.locks.set(resource, { lock }); + } catch (error: any) { + //based on: https://github.com/sesamecare/redlock/blob/508e00dcd1e4d2bc6373ce455f4fe847e98a9aab/src/index.ts#L347-L349 + if ( + error instanceof ExecutionError && + error.message === + "The operation was unable to achieve a quorum during its retry window." + ) { + // Expected behavior: Could not acquire lock, another instance locked it already. + // Skip further hooks and retry — the data is safe on the other instance. + throw new SkipFurtherHooksError( + "Another instance is already storing this document", + ); + } + //unexpected error + console.error("unexpected error:", error); + throw error; + } + } + + /** + * Release the Redis lock, so other instances can store documents. + */ + async afterStoreDocument({ + documentName, + lastTransactionOrigin, + }: afterStoreDocumentPayload) { + const lockKey = this.lockKey(documentName); + const lock = this.locks.get(lockKey); + if (lock) { + try { + // Always try to unlock and clean up the lock + lock.release = lock.lock.release(); + await lock.release; + } catch { + // Lock will expire on its own after timeout + } finally { + this.locks.delete(lockKey); + } + } + + // if the change was initiated by a directConnection (or otherwise local source), we need to delay this hook to make sure sync can finish first. + // for provider connections, this usually happens in the onDisconnect hook + if ( + isTransactionOrigin(lastTransactionOrigin) && + lastTransactionOrigin.source === "local" + ) { + const pending = this.pendingAfterStoreDocumentResolves.get(documentName); + + if (pending) { + clearTimeout(pending.timeout); + pending.resolve(); + this.pendingAfterStoreDocumentResolves.delete(documentName); + } + + let resolveFunction: () => void = () => {}; + const delayedPromise = new Promise((resolve) => { + resolveFunction = resolve; + }); + + const timeout = setTimeout(() => { + this.pendingAfterStoreDocumentResolves.delete(documentName); + resolveFunction(); + }, this.configuration.disconnectDelay); + + this.pendingAfterStoreDocumentResolves.set(documentName, { + timeout, + resolve: resolveFunction, + }); + + await delayedPromise; + } + } + + /** + * Handle awareness update messages received directly by this Hocuspocus instance. + */ + async onAwarenessUpdate({ + documentName, + awareness, + added, + updated, + removed, + document, + }: onAwarenessUpdatePayload) { + // Do not publish if there is no connection: it fixes this issue: "https://github.com/ueberdosis/hocuspocus/issues/1027" + const connections = document?.connections.size || 0; + if (connections === 0) { + return; // avoids exception + } + + const pending = this.schedulePublish(documentName, document); + + pending.awareness = awareness; + + for (const clientId of added.concat(updated, removed)) { + pending.awarenessClients.add(clientId); + } + } + + /** + * Handle incoming messages published on subscribed document channels. + * Note that this will also include messages from ourselves as it is not possible + * in Redis to filter these. + */ + private handleIncomingMessage = async (channel: Buffer, data: Buffer) => { + const [identifier, messageBuffer] = this.decodeMessage(data); + + if (identifier === this.configuration.identifier) { + return; + } + + const message = new IncomingMessage(messageBuffer); + const documentName = message.readVarString(); + message.writeVarString(documentName); + + // Prefer the extension-local map: it also contains documents that are + // still loading (not yet in `instance.documents`), which is exactly when + // a blocking `afterLoadDocument` needs to receive the peer's reply. + const document = + this.documents.get(documentName) ?? + this.instance.documents.get(documentName); + + if (!document) { + return; + } + + const carriesPeerState = this.messageCarriesPeerState(message); + + // State that reached us on our own reply channel was computed for our + // state vector; state overheard anywhere else was computed for somebody + // else and says nothing about whether we have caught up. + const addressedToUs = + channel.toString("utf-8") === + this.replyKey(this.configuration.identifier); + + const receiver = new MessageReceiver(message, this.redisTransactionOrigin); + await receiver.apply(document, undefined, (reply) => { + // Replies go to the requester alone, never to the document channel. + // A SyncStep2 is `encodeStateAsUpdate(document, requesterStateVector)`: + // the structs that one requester is missing plus the *entire* delete + // set. An instance whose state is behind the requester's applies those + // deletes without receiving the structs that replaced the deleted + // content, leaving it with a document that never existed — which it + // then broadcasts to its own clients and can persist (#1151). + return this.pub.publish( + this.replyKey(identifier), + this.encodeMessage(reply), + ); + }); + + // A peer answered our SyncStep1 with its state — the document has now + // caught up, so any blocking initial-sync wait can be released. + if (carriesPeerState && addressedToUs) { + this.resolveInitialSync(documentName); + } + }; + + /** + * if the ydoc changed, we'll need to inform other Hocuspocus servers about it. + */ + public async onChange(data: onChangePayload): Promise { + if ( + isTransactionOrigin(data.transactionOrigin) && + data.transactionOrigin.source === "redis" + ) { + return; + } + + this.schedulePublish(data.documentName, data.document).syncStepPending = + true; + } + + /** + * Delay unloading to allow syncs to finish + */ + async beforeUnloadDocument(data: beforeUnloadDocumentPayload) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, this.configuration.disconnectDelay); + }); + } + + async afterUnloadDocument(data: afterUnloadDocumentPayload) { + if (data.instance.documents.has(data.documentName)) return; // skip unsubscribe if the document is already loaded again (maybe fast reconnect) + + this.resolveInitialSync(data.documentName); + this.documents.delete(data.documentName); + // Nothing should be pending: the coalescing window is a single event loop + // turn and `beforeUnloadDocument` already waits `disconnectDelay` to let + // syncs finish. This is a safety net so a timer cannot fire against a + // document that is on its way out. + this.cancelPendingPublish(data.documentName); + + this.sub.unsubscribe(this.subKey(data.documentName), (error: any) => { + if (error) { + console.error(error); + } + }); + } + + async beforeBroadcastStateless(data: beforeBroadcastStatelessPayload) { + const message = new OutgoingMessage( + data.documentName, + ).writeBroadcastStateless(data.payload); + + return this.pub.publish( + this.pubKey(data.documentName), + this.encodeMessage(message.toUint8Array()), + ); + } + + /** + * Kill the Redlock connection immediately. + */ + async onDestroy() { + // Cancelled rather than flushed, because flushing could not achieve + // anything: a pending publish is a SyncStep1, i.e. a state vector, which + // only *asks* peers to reply. Our own changes reach them one round trip + // later, when we answer the SyncStep1 they send back — and the subscriber + // is disconnected a few lines below, so that answer would never come. + // Durability on shutdown comes from the fact that we flush per setImmediate, + // which runs before the delay in beforeUnloadDocument. + for (const documentName of Array.from(this.pendingPublishes.keys())) { + this.cancelPendingPublish(documentName); + } + + await this.redlock.quit(); + this.pub.disconnect(false); + this.sub.disconnect(false); + } +} diff --git a/packages/extension-redis/src/index.ts b/packages/extension-redis/src/index.ts new file mode 100644 index 0000000..6c9d48a --- /dev/null +++ b/packages/extension-redis/src/index.ts @@ -0,0 +1 @@ +export * from "./Redis.ts"; diff --git a/packages/extension-s3/CHANGELOG.md b/packages/extension-s3/CHANGELOG.md new file mode 100644 index 0000000..3aa94b2 --- /dev/null +++ b/packages/extension-s3/CHANGELOG.md @@ -0,0 +1,337 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-s3 + + + + + +# Changelog + +## [3.2.3] - 2024-08-10 + +### Added +- Initial release of @hocuspocus/extension-s3 +- Support for Amazon S3 and S3-compatible storage services (MinIO, DigitalOcean Spaces, etc.) +- Configurable bucket, region, and key prefix +- Support for custom S3 clients +- IAM role support for AWS environments +- Proper error handling for missing documents diff --git a/packages/extension-s3/README.md b/packages/extension-s3/README.md new file mode 100644 index 0000000..2ee7afc --- /dev/null +++ b/packages/extension-s3/README.md @@ -0,0 +1,171 @@ +# @hocuspocus/extension-s3 +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-s3.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-s3) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-s3.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-s3.svg)](https://www.npmjs.com/package/@hocuspocus/extension-s3) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +A S3-compatible persistence driver for [Hocuspocus](https://github.com/ueberdosis/hocuspocus). Stores Y.js documents in Amazon S3 or S3-compatible storage (MinIO, DigitalOcean Spaces, Cloudflare R2, Backblaze B2, etc.). + +## Installation + +```bash +npm install @hocuspocus/extension-s3 +``` + +## Quick Start + +### Basic AWS S3 Configuration + +```javascript +import { Server } from '@hocuspocus/server' +import { S3 } from '@hocuspocus/extension-s3' + +const server = new Server({ + extensions: [ + new S3({ + bucket: 'my-documents-bucket', + region: 'us-east-1', + credentials: { + accessKeyId: 'your-access-key', + secretAccessKey: 'your-secret-key' + } + }), + ], +}) + +server.listen() +``` + +### MinIO Configuration + +For local development with MinIO, ensure you set `forcePathStyle: true`: + +```javascript +const server = new Server({ + extensions: [ + new S3({ + bucket: 'hocuspocus-documents', + endpoint: 'http://localhost:9000', + forcePathStyle: true, + credentials: { + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin' + } + }), + ], +}) +``` + +## Configuration Options + +- **bucket** (required): S3 bucket name where documents will be stored +- **region**: AWS region (default: 'us-east-1') +- **prefix**: Key prefix for documents (default: 'hocuspocus-documents/') +- **credentials**: AWS credentials object with accessKeyId and secretAccessKey +- **endpoint**: S3 endpoint URL (for S3-compatible services) +- **forcePathStyle**: Use path-style URLs (required for MinIO, default: false) +- **s3Client**: Custom S3Client instance + +## Document Storage + +Documents are stored as binary files in S3 with the naming convention: +`{prefix}{documentName}.bin` + +## Scaling with Redis + +For horizontal scaling, combine S3 with the Redis extension. Redis handles real-time synchronization while S3 provides persistent storage. + +```javascript +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { Redis } from '@hocuspocus/extension-redis' +import { S3 } from '@hocuspocus/extension-s3' + +// Server 1 +const server1 = new Server({ + name: "server-1", + port: 8001, + extensions: [ + new Logger(), + new Redis({ + host: "127.0.0.1", + port: 6379, + }), + new S3({ + bucket: 'hocuspocus-documents', + endpoint: 'http://localhost:9000', // MinIO + forcePathStyle: true, + credentials: { + accessKeyId: 'minioadmin', + secretAccessKey: 'minioadmin' + } + }), + ], +}) + +// Server 2 - same configuration with different name/port +const server2 = new Server({ + name: "server-2", + port: 8002, + extensions: [/* same extensions */], +}) + +server1.listen() +server2.listen() +``` + +## CLI Usage + +You can also use the S3 extension with the Hocuspocus CLI: + +```bash +# Basic usage +hocuspocus --s3 --s3-bucket my-documents + +# MinIO setup (forcePathStyle automatically enabled) +hocuspocus --s3 --s3-bucket hocuspocus-documents --s3-endpoint http://localhost:9000 +``` + +## Development + +### Quick Start + +For local development with MinIO (S3-compatible storage): + +```bash +# Set up development environment +npm run dev:setup + +# Test S3 configuration +npm run dev:test-s3 + +# Run S3 playground examples +npm run playground:s3 +``` + +## Complete Documentation + +For detailed documentation, examples, best practices, and troubleshooting, see: +[📚 S3 Extension Documentation](https://tiptap.dev/docs/hocuspocus/server/extensions/s3) + +## IAM Permissions + +Your AWS credentials or IAM role needs the following permissions: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject", "s3:HeadObject"], + "Resource": "arn:aws:s3:::your-bucket-name/*" + }, + { + "Effect": "Allow", + "Action": ["s3:HeadBucket"], + "Resource": "arn:aws:s3:::your-bucket-name" + } + ] +} +``` \ No newline at end of file diff --git a/packages/extension-s3/package.json b/packages/extension-s3/package.json new file mode 100644 index 0000000..96ba98a --- /dev/null +++ b/packages/extension-s3/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hocuspocus/extension-s3", + "description": "a S3-compatible persistence driver for Hocuspocus", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "yjs", + "s3", + "persistence" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-s3.cjs", + "module": "dist/hocuspocus-s3.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-s3.esm.js", + "require": "./dist/hocuspocus-s3.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@aws-sdk/client-s3": "^3.990.0", + "@hocuspocus/extension-database": "workspace:^", + "kleur": "^4.1.5" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + } +} diff --git a/packages/extension-s3/src/S3.ts b/packages/extension-s3/src/S3.ts new file mode 100644 index 0000000..3015bd9 --- /dev/null +++ b/packages/extension-s3/src/S3.ts @@ -0,0 +1,197 @@ +import type { DatabaseConfiguration } from "@hocuspocus/extension-database"; +import { Database } from "@hocuspocus/extension-database"; +import { + S3Client, + GetObjectCommand, + PutObjectCommand, + HeadObjectCommand, +} from "@aws-sdk/client-s3"; +import kleur from "kleur"; + +export interface S3Configuration extends DatabaseConfiguration { + /** + * AWS S3 region + */ + region?: string; + /** + * S3 bucket name + */ + bucket: string; + /** + * S3 key prefix for documents (optional) + */ + prefix?: string; + /** + * AWS credentials + */ + credentials?: { + accessKeyId: string; + secretAccessKey: string; + }; + /** + * S3 endpoint URL (for S3-compatible services like MinIO) + */ + endpoint?: string; + /** + * Force path style URLs (required for MinIO) + */ + forcePathStyle?: boolean; + /** + * Custom S3 client + */ + s3Client?: S3Client; +} + +export class S3 extends Database { + private s3Client?: S3Client; + + configuration: S3Configuration = { + region: "us-east-1", + bucket: "", + prefix: "hocuspocus-documents/", + forcePathStyle: false, + fetch: async ({ documentName }) => { + const key = this.getObjectKey(documentName); + + try { + const command = new GetObjectCommand({ + Bucket: this.configuration.bucket, + Key: key, + }); + + const response = await this.s3Client!.send(command); + + if (response.Body) { + // Convert stream to Uint8Array + const chunks: Uint8Array[] = []; + const reader = response.Body.transformToWebStream().getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + // Combine all chunks into a single Uint8Array + const totalLength = chunks.reduce( + (acc, chunk) => acc + chunk.length, + 0, + ); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; + } + + return null; + } catch (error: any) { + if ( + error.name === "NoSuchKey" || + error.$metadata?.httpStatusCode === 404 + ) { + // Document doesn't exist yet, return null + return null; + } + throw error; + } + }, + store: async ({ documentName, state }) => { + const key = this.getObjectKey(documentName); + + const command = new PutObjectCommand({ + Bucket: this.configuration.bucket, + Key: key, + Body: state, + ContentType: "application/octet-stream", + }); + + await this.s3Client!.send(command); + }, + }; + + constructor(configuration: Partial) { + super({}); + + this.configuration = { + ...this.configuration, + ...configuration, + }; + + // Validate required configuration + if (!this.configuration.bucket) { + throw new Error("S3 bucket name is required"); + } + } + + private getObjectKey(documentName: string): string { + const prefix = this.configuration.prefix || ""; + return `${prefix}${documentName}.bin`; + } + + async onConfigure() { + // Use custom S3 client if provided, otherwise create one + if (this.configuration.s3Client) { + this.s3Client = this.configuration.s3Client; + } else { + const clientConfig: any = { + region: this.configuration.region, + }; + + if (this.configuration.credentials) { + clientConfig.credentials = this.configuration.credentials; + } + + if (this.configuration.endpoint) { + clientConfig.endpoint = this.configuration.endpoint; + clientConfig.forcePathStyle = this.configuration.forcePathStyle; + } + + this.s3Client = new S3Client(clientConfig); + } + + // Test S3 connection by checking if bucket exists + try { + const command = new HeadObjectCommand({ + Bucket: this.configuration.bucket, + Key: "test-connection", // This will likely return 404, but that's fine + }); + + await this.s3Client.send(command); + } catch (error: any) { + // 404 is expected for the test key, any other error indicates connection issues + if (error.$metadata?.httpStatusCode !== 404) { + // Don't show credential errors as connection failures in development + if (error.message?.includes("Could not load credentials")) { + console.warn(` ${kleur.yellow("S3 warning:")} ${error.message}`); + console.warn( + ` ${kleur.yellow("Note:")} Ensure AWS credentials are properly configured for production use`, + ); + } else { + console.error( + ` ${kleur.red("S3 connection failed:")} ${error.message}`, + ); + } + } + } + } + + async onListen() { + const endpoint = + this.configuration.endpoint || + `https://s3.${this.configuration.region}.amazonaws.com`; + console.log( + ` ${kleur.green("S3 extension configured:")} bucket=${this.configuration.bucket}, endpoint=${endpoint}`, + ); + + if (this.configuration.prefix) { + console.log( + ` ${kleur.blue("S3 key prefix:")} ${this.configuration.prefix}`, + ); + } + } +} diff --git a/packages/extension-s3/src/index.ts b/packages/extension-s3/src/index.ts new file mode 100644 index 0000000..f782642 --- /dev/null +++ b/packages/extension-s3/src/index.ts @@ -0,0 +1 @@ +export * from "./S3.ts"; diff --git a/packages/extension-sqlite/CHANGELOG.md b/packages/extension-sqlite/CHANGELOG.md new file mode 100644 index 0000000..15fdd76 --- /dev/null +++ b/packages/extension-sqlite/CHANGELOG.md @@ -0,0 +1,1273 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Bug Fixes + +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + +### Features + +* migrate from sqlite3 to better-sqlite3, as sqlite3 is now unmaintained. ([a247146](https://github.com/ueberdosis/hocuspocus/commit/a24714624c6effdb521e84352f0623d24be5d3f0)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + + +### Bug Fixes + +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate from sqlite3 to better-sqlite3, as sqlite3 is now unmaintained. ([a247146](https://github.com/ueberdosis/hocuspocus/commit/a24714624c6effdb521e84352f0623d24be5d3f0)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.20...@hocuspocus/extension-sqlite@1.0.0-alpha.21) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.19...@hocuspocus/extension-sqlite@1.0.0-alpha.20) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.18...@hocuspocus/extension-sqlite@1.0.0-alpha.19) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.17...@hocuspocus/extension-sqlite@1.0.0-alpha.18) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.16...@hocuspocus/extension-sqlite@1.0.0-alpha.17) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.15...@hocuspocus/extension-sqlite@1.0.0-alpha.16) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.14...@hocuspocus/extension-sqlite@1.0.0-alpha.15) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.13...@hocuspocus/extension-sqlite@1.0.0-alpha.14) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.12...@hocuspocus/extension-sqlite@1.0.0-alpha.13) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.11...@hocuspocus/extension-sqlite@1.0.0-alpha.12) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.10...@hocuspocus/extension-sqlite@1.0.0-alpha.11) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.9...@hocuspocus/extension-sqlite@1.0.0-alpha.10) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.8...@hocuspocus/extension-sqlite@1.0.0-alpha.9) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.7...@hocuspocus/extension-sqlite@1.0.0-alpha.8) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.6...@hocuspocus/extension-sqlite@1.0.0-alpha.7) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.5...@hocuspocus/extension-sqlite@1.0.0-alpha.6) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.4...@hocuspocus/extension-sqlite@1.0.0-alpha.5) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.3...@hocuspocus/extension-sqlite@1.0.0-alpha.4) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.2...@hocuspocus/extension-sqlite@1.0.0-alpha.3) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# [1.0.0-alpha.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-sqlite@1.0.0-alpha.1...@hocuspocus/extension-sqlite@1.0.0-alpha.2) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite + + + + + +# 1.0.0-alpha.1 (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-sqlite diff --git a/packages/extension-sqlite/README.md b/packages/extension-sqlite/README.md new file mode 100644 index 0000000..e63027b --- /dev/null +++ b/packages/extension-sqlite/README.md @@ -0,0 +1,55 @@ +# @hocuspocus/extension-sqlite +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-sqlite.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-sqlite) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-sqlite.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-sqlite.svg)](https://www.npmjs.com/package/@hocuspocus/extension-sqlite) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +Persist [Hocuspocus](https://github.com/ueberdosis/hocuspocus) documents to a local SQLite database. Zero-config storage for development, self-hosted single-instance deployments, and small production setups. Uses [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3). + +## Installation + +```bash +npm install @hocuspocus/extension-sqlite +``` + +## Usage + +```js +import { Server } from "@hocuspocus/server" +import { SQLite } from "@hocuspocus/extension-sqlite" + +const server = new Server({ + extensions: [ + new SQLite({ database: "db.sqlite" }), + ], +}) + +server.listen() +``` + +`database` accepts: +- a filename (persisted to disk, e.g. `"db.sqlite"`) +- `":memory:"` for an in-memory database +- `""` for an anonymous on-disk database + +Omit the option entirely to default to `":memory:"`. + +## Troubleshooting + +### `Could not locate the bindings file` + +`better-sqlite3` ships prebuilt native addons, but they don't always cover the newest Node.js release on day one. Rebuild from source in your server project: + +```bash +npm rebuild better-sqlite3 --build-from-source +``` + +Needs a C++ toolchain (Xcode Command Line Tools on macOS, `build-essential` on Debian/Ubuntu, Visual Studio Build Tools on Windows). + +## Documentation + +Schema, custom `fetch`/`store`, and migration guides: [tiptap.dev/docs/hocuspocus/server/extensions/sqlite](https://tiptap.dev/docs/hocuspocus/server/extensions/sqlite). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/extension-sqlite/package.json b/packages/extension-sqlite/package.json new file mode 100644 index 0000000..33ec629 --- /dev/null +++ b/packages/extension-sqlite/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hocuspocus/extension-sqlite", + "description": "a generic Hocuspocus persistence driver for the sqlite", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-sqlite.cjs", + "module": "dist/hocuspocus-sqlite.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-sqlite.esm.js", + "require": "./dist/hocuspocus-sqlite.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/extension-database": "workspace:^", + "better-sqlite3": "^12.6.2", + "kleur": "^4.1.5" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13" + }, + "publishConfig": { + "access": "public" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + } +} diff --git a/packages/extension-sqlite/src/SQLite.ts b/packages/extension-sqlite/src/SQLite.ts new file mode 100644 index 0000000..7919449 --- /dev/null +++ b/packages/extension-sqlite/src/SQLite.ts @@ -0,0 +1,81 @@ +import type { DatabaseConfiguration } from "@hocuspocus/extension-database"; +import { Database } from "@hocuspocus/extension-database"; +import BetterSqlite3 from "better-sqlite3"; +import kleur from "kleur"; + +export const schema = `CREATE TABLE IF NOT EXISTS "documents" ( + "name" varchar(255) NOT NULL, + "data" blob NOT NULL, + UNIQUE(name) +)`; + +export const selectQuery = ` + SELECT data FROM "documents" WHERE name = $name ORDER BY rowid DESC +`; + +export const upsertQuery = ` + INSERT INTO "documents" ("name", "data") VALUES ($name, $data) + ON CONFLICT(name) DO UPDATE SET data = $data +`; + +const SQLITE_INMEMORY = ":memory:"; + +export interface SQLiteConfiguration extends DatabaseConfiguration { + /** + * Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty + * string for an anonymous disk-based database. Anonymous databases are not persisted and + * when closing the database handle, their contents are lost. + * + * https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#new-daboratabasepath-options + */ + database: string; + /** + * The database schema to create. + */ + schema: string; +} + +export class SQLite extends Database { + db?: BetterSqlite3.Database; + + configuration: SQLiteConfiguration = { + database: SQLITE_INMEMORY, + schema, + fetch: async ({ documentName }) => { + const row = this.db + ?.prepare(selectQuery) + .get({ name: documentName }) as { data: Buffer } | undefined; + + return row?.data ?? null; + }, + store: async ({ documentName, state }) => { + this.db?.prepare(upsertQuery).run({ + name: documentName, + data: state, + }); + }, + }; + + constructor(configuration?: Partial) { + super({}); + + this.configuration = { + ...this.configuration, + ...configuration, + }; + } + + async onConfigure() { + this.db = new BetterSqlite3(this.configuration.database); + this.db.exec(this.configuration.schema); + } + + async onListen() { + if (this.configuration.database === SQLITE_INMEMORY) { + console.warn( + ` ${kleur.yellow("The SQLite extension is configured as an in-memory database. All changes will be lost on restart!")}`, + ); + console.log(); + } + } +} diff --git a/packages/extension-sqlite/src/index.ts b/packages/extension-sqlite/src/index.ts new file mode 100644 index 0000000..8013efb --- /dev/null +++ b/packages/extension-sqlite/src/index.ts @@ -0,0 +1 @@ +export * from "./SQLite.ts"; diff --git a/packages/extension-throttle/CHANGELOG.md b/packages/extension-throttle/CHANGELOG.md new file mode 100644 index 0000000..e54e3e7 --- /dev/null +++ b/packages/extension-throttle/CHANGELOG.md @@ -0,0 +1,1593 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.63](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.62...@hocuspocus/extension-throttle@1.0.0-alpha.63) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.62](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.61...@hocuspocus/extension-throttle@1.0.0-alpha.62) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.61](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.60...@hocuspocus/extension-throttle@1.0.0-alpha.61) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.60](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.59...@hocuspocus/extension-throttle@1.0.0-alpha.60) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.59](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.58...@hocuspocus/extension-throttle@1.0.0-alpha.59) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.58](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.57...@hocuspocus/extension-throttle@1.0.0-alpha.58) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.57](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.56...@hocuspocus/extension-throttle@1.0.0-alpha.57) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.56](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.55...@hocuspocus/extension-throttle@1.0.0-alpha.56) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.55](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.54...@hocuspocus/extension-throttle@1.0.0-alpha.55) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.54](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.53...@hocuspocus/extension-throttle@1.0.0-alpha.54) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.53](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.52...@hocuspocus/extension-throttle@1.0.0-alpha.53) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.52](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.51...@hocuspocus/extension-throttle@1.0.0-alpha.52) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.51](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.50...@hocuspocus/extension-throttle@1.0.0-alpha.51) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.50](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.49...@hocuspocus/extension-throttle@1.0.0-alpha.50) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.49](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.48...@hocuspocus/extension-throttle@1.0.0-alpha.49) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.48](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.47...@hocuspocus/extension-throttle@1.0.0-alpha.48) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.47](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.46...@hocuspocus/extension-throttle@1.0.0-alpha.47) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.46](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.45...@hocuspocus/extension-throttle@1.0.0-alpha.46) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.45](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.44...@hocuspocus/extension-throttle@1.0.0-alpha.45) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.44](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.43...@hocuspocus/extension-throttle@1.0.0-alpha.44) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.43](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.42...@hocuspocus/extension-throttle@1.0.0-alpha.43) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.42](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.41...@hocuspocus/extension-throttle@1.0.0-alpha.42) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.41](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.40...@hocuspocus/extension-throttle@1.0.0-alpha.41) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.39...@hocuspocus/extension-throttle@1.0.0-alpha.40) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.38...@hocuspocus/extension-throttle@1.0.0-alpha.39) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.37...@hocuspocus/extension-throttle@1.0.0-alpha.38) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.36...@hocuspocus/extension-throttle@1.0.0-alpha.37) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.35...@hocuspocus/extension-throttle@1.0.0-alpha.36) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.34...@hocuspocus/extension-throttle@1.0.0-alpha.35) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.34](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.33...@hocuspocus/extension-throttle@1.0.0-alpha.34) (2021-11-10) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.32...@hocuspocus/extension-throttle@1.0.0-alpha.33) (2021-11-05) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.31...@hocuspocus/extension-throttle@1.0.0-alpha.32) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.30...@hocuspocus/extension-throttle@1.0.0-alpha.31) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.29...@hocuspocus/extension-throttle@1.0.0-alpha.30) (2021-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.28...@hocuspocus/extension-throttle@1.0.0-alpha.29) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.27...@hocuspocus/extension-throttle@1.0.0-alpha.28) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.26...@hocuspocus/extension-throttle@1.0.0-alpha.27) (2021-09-17) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.25...@hocuspocus/extension-throttle@1.0.0-alpha.26) (2021-09-14) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.24...@hocuspocus/extension-throttle@1.0.0-alpha.25) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.23...@hocuspocus/extension-throttle@1.0.0-alpha.24) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.22...@hocuspocus/extension-throttle@1.0.0-alpha.23) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.21...@hocuspocus/extension-throttle@1.0.0-alpha.22) (2021-08-29) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.20...@hocuspocus/extension-throttle@1.0.0-alpha.21) (2021-08-27) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.19...@hocuspocus/extension-throttle@1.0.0-alpha.20) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.18...@hocuspocus/extension-throttle@1.0.0-alpha.19) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.17...@hocuspocus/extension-throttle@1.0.0-alpha.18) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.16...@hocuspocus/extension-throttle@1.0.0-alpha.17) (2021-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.15...@hocuspocus/extension-throttle@1.0.0-alpha.16) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.14...@hocuspocus/extension-throttle@1.0.0-alpha.15) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.13...@hocuspocus/extension-throttle@1.0.0-alpha.14) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.12...@hocuspocus/extension-throttle@1.0.0-alpha.13) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.11...@hocuspocus/extension-throttle@1.0.0-alpha.12) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.10...@hocuspocus/extension-throttle@1.0.0-alpha.11) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.9...@hocuspocus/extension-throttle@1.0.0-alpha.10) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.8...@hocuspocus/extension-throttle@1.0.0-alpha.9) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.7...@hocuspocus/extension-throttle@1.0.0-alpha.8) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.6...@hocuspocus/extension-throttle@1.0.0-alpha.7) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.5...@hocuspocus/extension-throttle@1.0.0-alpha.6) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.4...@hocuspocus/extension-throttle@1.0.0-alpha.5) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.3...@hocuspocus/extension-throttle@1.0.0-alpha.4) (2021-04-15) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-throttle@1.0.0-alpha.2...@hocuspocus/extension-throttle@1.0.0-alpha.3) (2021-04-14) + +**Note:** Version bump only for package @hocuspocus/extension-throttle + + + + + +# 1.0.0-alpha.2 (2021-04-12) + + +### Features + +* add throttle extension ([b65de2a](https://github.com/ueberdosis/hocuspocus/commit/b65de2aa127c79fcad433d5e7353face7ad82d26)) diff --git a/packages/extension-throttle/README.md b/packages/extension-throttle/README.md new file mode 100644 index 0000000..64bd60b --- /dev/null +++ b/packages/extension-throttle/README.md @@ -0,0 +1,48 @@ +# @hocuspocus/extension-throttle +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-throttle.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-throttle) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-throttle.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-throttle.svg)](https://www.npmjs.com/package/@hocuspocus/extension-throttle) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +Rate-limit and throttle connections to a [Hocuspocus](https://github.com/ueberdosis/hocuspocus) server by IP. Protects your collaboration backend from abuse, accidental reconnect loops, and misbehaving clients. + +## Installation + +```bash +npm install @hocuspocus/extension-throttle +``` + +## Usage + +With default thresholds (15 connection attempts per 60 seconds, 5 minute ban): + +```js +import { Server } from "@hocuspocus/server" +import { Throttle } from "@hocuspocus/extension-throttle" + +const server = new Server({ + extensions: [new Throttle()], +}) + +server.listen() +``` + +### Tune the limits + +```js +new Throttle({ + throttle: 30, // allowed connection attempts per window + consideredSeconds: 60, // window length in seconds + banTime: 5, // ban duration in minutes +}) +``` + +Pass `throttle: false` (or `null`) to effectively disable the limiter without removing the extension. + +## Documentation + +Full options reference: [tiptap.dev/docs/hocuspocus/server/extensions/throttle](https://tiptap.dev/docs/hocuspocus/server/extensions/throttle). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/extension-throttle/package.json b/packages/extension-throttle/package.json new file mode 100644 index 0000000..c77413e --- /dev/null +++ b/packages/extension-throttle/package.json @@ -0,0 +1,40 @@ +{ + "name": "@hocuspocus/extension-throttle", + "version": "4.7.0", + "description": "hocuspocus throttle extension", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "throttle", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-throttle.cjs", + "module": "dist/hocuspocus-throttle.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-throttle.esm.js", + "require": "./dist/hocuspocus-throttle.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/server": "workspace:^" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/extension-throttle/src/index.ts b/packages/extension-throttle/src/index.ts new file mode 100644 index 0000000..45e4a96 --- /dev/null +++ b/packages/extension-throttle/src/index.ts @@ -0,0 +1,126 @@ +import type { Extension, onConnectPayload } from "@hocuspocus/server"; + +export interface ThrottleConfiguration { + throttle: number | null | false; // how many requests within `consideredSeconds` until we're rejecting requests (setting this to 15 means the 16th request will be rejected) + consideredSeconds: number; // how many seconds to consider (default is last 60 seconds from the current connection attempt) + banTime: number; // for how long to ban after receiving too many requests (in minutes!) + cleanupInterval: number; // how often to clean up the records of IPs (this won't delete ips that are still blocked or recent enough by `consideredSeconds`) +} + +export class Throttle implements Extension { + configuration: ThrottleConfiguration = { + throttle: 15, + banTime: 5, + consideredSeconds: 60, + cleanupInterval: 90, + }; + + connectionsByIp: Map> = new Map(); + + bannedIps: Map = new Map(); + + cleanupInterval?: NodeJS.Timeout; + + /** + * Constructor + */ + constructor(configuration?: Partial) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + + this.cleanupInterval = setInterval( + this.clearMaps.bind(this), + this.configuration.cleanupInterval * 1000, + ); + } + + onDestroy() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + + return Promise.resolve(); + } + + public clearMaps() { + this.connectionsByIp.forEach((value, key) => { + const filteredValue = value.filter( + (timestamp) => + timestamp + this.configuration.consideredSeconds * 1000 > Date.now(), + ); + + if (filteredValue.length) { + this.connectionsByIp.set(key, filteredValue); + } else { + this.connectionsByIp.delete(key); + } + }); + + this.bannedIps.forEach((value, key) => { + if (!this.isBanned(key)) { + this.bannedIps.delete(key); + } + }); + } + + isBanned(ip: string) { + const bannedAt = this.bannedIps.get(ip) || 0; + return Date.now() < bannedAt + this.configuration.banTime * 60 * 1000; + } + + /** + * Throttle requests + * @private + */ + private throttle(ip: string): boolean { + if (!this.configuration.throttle) { + return false; + } + + if (this.isBanned(ip)) return true; + + this.bannedIps.delete(ip); + + // add this connection try to the list of previous connections + const previousConnections = this.connectionsByIp.get(ip) || []; + previousConnections.push(Date.now()); + + // calculate the previous connections in the last considered time interval + const previousConnectionsInTheConsideredInterval = + previousConnections.filter( + (timestamp) => + timestamp + this.configuration.consideredSeconds * 1000 > Date.now(), + ); + + this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval); + + if ( + previousConnectionsInTheConsideredInterval.length > + this.configuration.throttle + ) { + this.bannedIps.set(ip, Date.now()); + return true; + } + + return false; + } + + /** + * onConnect hook + * @param data + */ + onConnect(data: onConnectPayload): Promise { + const { request } = data; + + // get the remote ip address from headers (web standard Request doesn't have socket) + const ip = + request.headers.get("x-real-ip") || + request.headers.get("x-forwarded-for") || + ""; + + // throttle the connection + return this.throttle(ip) ? Promise.reject() : Promise.resolve(); + } +} diff --git a/packages/extension-webhook/CHANGELOG.md b/packages/extension-webhook/CHANGELOG.md new file mode 100644 index 0000000..1398d7d --- /dev/null +++ b/packages/extension-webhook/CHANGELOG.md @@ -0,0 +1,1666 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Features + +* remove axios ([#1089](https://github.com/ueberdosis/hocuspocus/issues/1089)) ([fe99d02](https://github.com/ueberdosis/hocuspocus/commit/fe99d02496cae65f9c3c4f1b37c0c6823f05f95a)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Features + +* remove uuid dependency and replace usages by crypto.randomUUID ([#1005](https://github.com/ueberdosis/hocuspocus/issues/1005)) ([393189f](https://github.com/ueberdosis/hocuspocus/commit/393189fe448c68cbd741642866be4b4b23546f17)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.71](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.70...@hocuspocus/extension-webhook@1.0.0-alpha.71) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.70](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.69...@hocuspocus/extension-webhook@1.0.0-alpha.70) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.69](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.68...@hocuspocus/extension-webhook@1.0.0-alpha.69) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.68](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.67...@hocuspocus/extension-webhook@1.0.0-alpha.68) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.67](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.66...@hocuspocus/extension-webhook@1.0.0-alpha.67) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.66](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.65...@hocuspocus/extension-webhook@1.0.0-alpha.66) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.65](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.64...@hocuspocus/extension-webhook@1.0.0-alpha.65) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.64](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.63...@hocuspocus/extension-webhook@1.0.0-alpha.64) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.63](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.62...@hocuspocus/extension-webhook@1.0.0-alpha.63) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.62](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.61...@hocuspocus/extension-webhook@1.0.0-alpha.62) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.61](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.60...@hocuspocus/extension-webhook@1.0.0-alpha.61) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.60](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.59...@hocuspocus/extension-webhook@1.0.0-alpha.60) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.59](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.58...@hocuspocus/extension-webhook@1.0.0-alpha.59) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.58](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.57...@hocuspocus/extension-webhook@1.0.0-alpha.58) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.57](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.56...@hocuspocus/extension-webhook@1.0.0-alpha.57) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.56](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.55...@hocuspocus/extension-webhook@1.0.0-alpha.56) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.55](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.54...@hocuspocus/extension-webhook@1.0.0-alpha.55) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.54](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.53...@hocuspocus/extension-webhook@1.0.0-alpha.54) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.53](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.52...@hocuspocus/extension-webhook@1.0.0-alpha.53) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.52](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.51...@hocuspocus/extension-webhook@1.0.0-alpha.52) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.51](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.50...@hocuspocus/extension-webhook@1.0.0-alpha.51) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.50](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.49...@hocuspocus/extension-webhook@1.0.0-alpha.50) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.49](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.48...@hocuspocus/extension-webhook@1.0.0-alpha.49) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.48](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.47...@hocuspocus/extension-webhook@1.0.0-alpha.48) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.47](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.46...@hocuspocus/extension-webhook@1.0.0-alpha.47) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.46](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.45...@hocuspocus/extension-webhook@1.0.0-alpha.46) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.45](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.44...@hocuspocus/extension-webhook@1.0.0-alpha.45) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.44](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.43...@hocuspocus/extension-webhook@1.0.0-alpha.44) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.43](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.42...@hocuspocus/extension-webhook@1.0.0-alpha.43) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.42](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.41...@hocuspocus/extension-webhook@1.0.0-alpha.42) (2021-11-10) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.41](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.40...@hocuspocus/extension-webhook@1.0.0-alpha.41) (2021-11-05) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.39...@hocuspocus/extension-webhook@1.0.0-alpha.40) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.38...@hocuspocus/extension-webhook@1.0.0-alpha.39) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.37...@hocuspocus/extension-webhook@1.0.0-alpha.38) (2021-10-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.36...@hocuspocus/extension-webhook@1.0.0-alpha.37) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.35...@hocuspocus/extension-webhook@1.0.0-alpha.36) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.34...@hocuspocus/extension-webhook@1.0.0-alpha.35) (2021-09-17) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.34](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.33...@hocuspocus/extension-webhook@1.0.0-alpha.34) (2021-09-14) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.32...@hocuspocus/extension-webhook@1.0.0-alpha.33) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.31...@hocuspocus/extension-webhook@1.0.0-alpha.32) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.30...@hocuspocus/extension-webhook@1.0.0-alpha.31) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.29...@hocuspocus/extension-webhook@1.0.0-alpha.30) (2021-08-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.28...@hocuspocus/extension-webhook@1.0.0-alpha.29) (2021-08-27) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.27...@hocuspocus/extension-webhook@1.0.0-alpha.28) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.26...@hocuspocus/extension-webhook@1.0.0-alpha.27) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.25...@hocuspocus/extension-webhook@1.0.0-alpha.26) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.24...@hocuspocus/extension-webhook@1.0.0-alpha.25) (2021-07-13) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.23...@hocuspocus/extension-webhook@1.0.0-alpha.24) (2021-07-07) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.22...@hocuspocus/extension-webhook@1.0.0-alpha.23) (2021-06-23) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.21...@hocuspocus/extension-webhook@1.0.0-alpha.22) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.20...@hocuspocus/extension-webhook@1.0.0-alpha.21) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.19...@hocuspocus/extension-webhook@1.0.0-alpha.20) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.18...@hocuspocus/extension-webhook@1.0.0-alpha.19) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.17...@hocuspocus/extension-webhook@1.0.0-alpha.18) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.16...@hocuspocus/extension-webhook@1.0.0-alpha.17) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.15...@hocuspocus/extension-webhook@1.0.0-alpha.16) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.14...@hocuspocus/extension-webhook@1.0.0-alpha.15) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.13...@hocuspocus/extension-webhook@1.0.0-alpha.14) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.12...@hocuspocus/extension-webhook@1.0.0-alpha.13) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.11...@hocuspocus/extension-webhook@1.0.0-alpha.12) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.10...@hocuspocus/extension-webhook@1.0.0-alpha.11) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.9...@hocuspocus/extension-webhook@1.0.0-alpha.10) (2021-04-15) + + +### Features + +* add request headers and parameters to onCreateDocument ([47a8b95](https://github.com/ueberdosis/hocuspocus/commit/47a8b95baf8dd22ebd71c56565420179402cdaa4)) + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.8...@hocuspocus/extension-webhook@1.0.0-alpha.9) (2021-04-14) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.7...@hocuspocus/extension-webhook@1.0.0-alpha.8) (2021-04-12) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.6...@hocuspocus/extension-webhook@1.0.0-alpha.7) (2021-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.5...@hocuspocus/extension-webhook@1.0.0-alpha.6) (2021-04-08) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.4...@hocuspocus/extension-webhook@1.0.0-alpha.5) (2021-04-07) + + +### Features + +* add connect event to webhook extension ([d907354](https://github.com/ueberdosis/hocuspocus/commit/d907354561ca31130061b6393c240598d67259b1)) +* add create event to import documents from api ([240a032](https://github.com/ueberdosis/hocuspocus/commit/240a032a8d839144f9662c1ec947958823abfd45)) +* add disconnect event to webhook extension ([81679dd](https://github.com/ueberdosis/hocuspocus/commit/81679dd7828e96fc6e0a279880379eb16b28d788)) + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.3...@hocuspocus/extension-webhook@1.0.0-alpha.4) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/extension-webhook@1.0.0-alpha.2...@hocuspocus/extension-webhook@1.0.0-alpha.3) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/extension-webhook + + + + + +# 1.0.0-alpha.2 (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/extension-webhook diff --git a/packages/extension-webhook/README.md b/packages/extension-webhook/README.md new file mode 100644 index 0000000..4ab2994 --- /dev/null +++ b/packages/extension-webhook/README.md @@ -0,0 +1,44 @@ +# @hocuspocus/extension-webhook +[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-webhook.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-webhook) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-webhook.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/extension-webhook.svg)](https://www.npmjs.com/package/@hocuspocus/extension-webhook) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +Send HTTP webhooks for [Hocuspocus](https://github.com/ueberdosis/hocuspocus) document lifecycle events — document changes, connects, disconnects, and creation. Lets you integrate the collaboration backend with your own services (search indexing, audit logs, downstream data pipelines) without writing a custom extension. + +## Installation + +```bash +npm install @hocuspocus/extension-webhook +``` + +## Usage + +```js +import { Server } from "@hocuspocus/server" +import { Webhook, Events } from "@hocuspocus/extension-webhook" + +const server = new Server({ + extensions: [ + new Webhook({ + url: "https://example.com/hocuspocus-webhook", + secret: "your-signing-secret", + events: [Events.onChange, Events.onConnect, Events.onDisconnect, Events.onCreate], + }), + ], +}) + +server.listen() +``` + +Requests are signed with HMAC-SHA256 using your `secret`, sent in the `X-Hocuspocus-Signature-256` header so the receiver can verify authenticity. + +`onChange` events are debounced (default `2000ms`, max wait `10000ms`) to avoid a request per keystroke — tune with the `debounce` and `debounceMaxWait` options. + +## Documentation + +Payload shapes, signature verification, and debounce tuning: [tiptap.dev/docs/hocuspocus/server/extensions/webhook](https://tiptap.dev/docs/hocuspocus/server/extensions/webhook). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/extension-webhook/package.json b/packages/extension-webhook/package.json new file mode 100644 index 0000000..8df2543 --- /dev/null +++ b/packages/extension-webhook/package.json @@ -0,0 +1,47 @@ +{ + "name": "@hocuspocus/extension-webhook", + "version": "4.7.0", + "description": "hocuspocus webhook extension", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "webhook", + "api", + "rest", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-webhook.cjs", + "module": "dist/hocuspocus-webhook.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-webhook.esm.js", + "require": "./dist/hocuspocus-webhook.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/common": "workspace:^", + "@hocuspocus/server": "workspace:^", + "@hocuspocus/transformer": "workspace:^" + }, + "peerDependencies": { + "yjs": "^13.6.8" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/extension-webhook/src/index.ts b/packages/extension-webhook/src/index.ts new file mode 100644 index 0000000..f58f18d --- /dev/null +++ b/packages/extension-webhook/src/index.ts @@ -0,0 +1,227 @@ +import { createHmac } from "node:crypto"; +import { Forbidden } from "@hocuspocus/common"; +import type { + Extension, + onChangePayload, + onConnectPayload, + onDisconnectPayload, + onLoadDocumentPayload, +} from "@hocuspocus/server"; +import type { Transformer } from "@hocuspocus/transformer"; +import { TiptapTransformer } from "@hocuspocus/transformer"; +import type { Doc } from "yjs"; + +export enum Events { + onChange = "change", + onConnect = "connect", + onCreate = "create", + onDisconnect = "disconnect", +} + +export interface Configuration { + debounce: number | false | null; + debounceMaxWait: number; + secret: string; + transformer: + | Transformer + | { + toYdoc: (document: any) => Doc; + fromYdoc: (document: Doc) => any; + }; + url: string; + events: Array; +} + +export class Webhook implements Extension { + configuration: Configuration = { + debounce: 2000, + debounceMaxWait: 10000, + secret: "", + transformer: TiptapTransformer, + url: "", + events: [Events.onChange], + }; + + debounced: Map = + new Map(); + + /** + * Constructor + */ + constructor(configuration?: Partial) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + + if (!this.configuration.url) { + throw new Error("url is required!"); + } + } + + /** + * Create a signature for the response body + */ + createSignature(body: string): string { + const hmac = createHmac("sha256", this.configuration.secret); + + return `sha256=${hmac.update(body).digest("hex")}`; + } + + /** + * debounce the given function, using the given identifier + */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + debounce(id: string, func: Function) { + const old = this.debounced.get(id); + const start = old?.start || Date.now(); + + const run = () => { + this.debounced.delete(id); + func(); + }; + + if (old?.timeout) clearTimeout(old.timeout); + if (Date.now() - start >= this.configuration.debounceMaxWait) return run(); + + this.debounced.set(id, { + start, + timeout: setTimeout(run, this.configuration.debounce), + }); + } + + /** + * Send a request to the given url containing the given data + */ + async sendRequest(event: Events, payload: any) { + const json = JSON.stringify({ event, payload }); + + const response = await fetch(this.configuration.url, { + method: "POST", + body: json, + headers: { + "X-Hocuspocus-Signature-256": this.createSignature(json), + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Webhook request to ${this.configuration.url} failed with status ${response.status}`, + ); + } + + const text = await response.text(); + const contentType = response.headers.get("content-type") ?? ""; + const data = + contentType.includes("application/json") && text + ? JSON.parse(text) + : text; + + return { status: response.status, data }; + } + + /** + * onChange hook + */ + async onChange(data: onChangePayload) { + if (!this.configuration.events.includes(Events.onChange)) { + return; + } + + const save = async () => { + try { + await this.sendRequest(Events.onChange, { + document: this.configuration.transformer.fromYdoc(data.document), + documentName: data.documentName, + context: data.context, + requestHeaders: data.requestHeaders, + requestParameters: Object.fromEntries( + data.requestParameters.entries(), + ), + }); + } catch (e) { + console.error(`Caught error in extension-webhook: ${e}`); + } + }; + + if (!this.configuration.debounce) { + return save(); + } + + this.debounce(data.documentName, save); + } + + /** + * onLoadDocument hook + */ + async onLoadDocument(data: onLoadDocumentPayload) { + if (!this.configuration.events.includes(Events.onCreate)) { + return; + } + + try { + const response = await this.sendRequest(Events.onCreate, { + documentName: data.documentName, + requestHeaders: data.requestHeaders, + requestParameters: Object.fromEntries(data.requestParameters.entries()), + }); + + const document = response.data; + if (!document) return; + + // eslint-disable-next-line guard-for-in,no-restricted-syntax + for (const fieldName in document) { + if (data.document.isEmpty(fieldName)) { + data.document.merge( + this.configuration.transformer.toYdoc( + document[fieldName], + fieldName, + ), + ); + } + } + } catch (e) { + console.error(`Caught error in extension-webhook: ${e}`); + } + } + + /** + * onConnect hook + */ + async onConnect(data: onConnectPayload) { + if (!this.configuration.events.includes(Events.onConnect)) { + return; + } + + try { + const response = await this.sendRequest(Events.onConnect, { + documentName: data.documentName, + requestHeaders: data.requestHeaders, + requestParameters: Object.fromEntries(data.requestParameters.entries()), + }); + + return response.data; + } catch (e) { + console.error(`Caught error in extension-webhook: ${e}`); + throw Forbidden; + } + } + + async onDisconnect(data: onDisconnectPayload) { + if (!this.configuration.events.includes(Events.onDisconnect)) { + return; + } + + try { + await this.sendRequest(Events.onDisconnect, { + documentName: data.documentName, + requestHeaders: data.requestHeaders, + requestParameters: Object.fromEntries(data.requestParameters.entries()), + context: data.context, + }); + } catch (e) { + console.error(`Caught error in extension-webhook: ${e}`); + } + } +} diff --git a/packages/provider-react/CHANGELOG.md b/packages/provider-react/CHANGELOG.md new file mode 100644 index 0000000..1e85a2e --- /dev/null +++ b/packages/provider-react/CHANGELOG.md @@ -0,0 +1,138 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + + +### Features + +* add `flushDelay` option for batching updates to reduce websocket traffic during heavy editing ([#1118](https://github.com/ueberdosis/hocuspocus/issues/1118)) ([75594c0](https://github.com/ueberdosis/hocuspocus/commit/75594c05d57d48f2f70d4c9440c28b8226bf95ac)) + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/provider-react + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + + +### Bug Fixes + +* react provider in strict mode ([4f27b7f](https://github.com/ueberdosis/hocuspocus/commit/4f27b7f0e57423b08fabb59cfa6d0a2cc67bc996)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + + +### Bug Fixes + +* publishConfig for provider-react ([376765f](https://github.com/ueberdosis/hocuspocus/commit/376765f6623259c79940da67a1f970ac9a1e84b1)) + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Features + +* first draft of @hocuspocus/provider-react package ([#1059](https://github.com/ueberdosis/hocuspocus/issues/1059)) ([cbc9977](https://github.com/ueberdosis/hocuspocus/commit/cbc99770096e8c674dae304c7e1a16e0fbec827b)) diff --git a/packages/provider-react/README.md b/packages/provider-react/README.md new file mode 100644 index 0000000..afd5c2b --- /dev/null +++ b/packages/provider-react/README.md @@ -0,0 +1,70 @@ +# @hocuspocus/provider-react +[![Version](https://img.shields.io/npm/v/@hocuspocus/provider-react.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/provider-react) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/provider-react.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/provider-react.svg)](https://www.npmjs.com/package/@hocuspocus/provider-react) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +React bindings for the [Hocuspocus provider](../provider). Wraps the provider in components and hooks so React handles the lifecycle — including StrictMode double-mounts. Built on `useSyncExternalStore`. + +## Installation + +```bash +npm install @hocuspocus/provider @hocuspocus/provider-react yjs +``` + +Requires React 18 or 19. + +## Usage + +Wrap your collaborative subtree with `HocuspocusProviderWebsocketComponent` (shared WebSocket) and one or more `HocuspocusRoom`s (one per document). Inside the room, hooks give you the provider and its state: + +```tsx +import { + HocuspocusProviderWebsocketComponent, + HocuspocusRoom, + useHocuspocusProvider, + useHocuspocusConnectionStatus, +} from "@hocuspocus/provider-react" + +function Editor() { + const provider = useHocuspocusProvider() + const status = useHocuspocusConnectionStatus() + // wire provider.document / provider.awareness into your editor + return
{status}
+} + +export function App() { + return ( + + + + + + ) +} +``` + +To use with Tiptap, pass `provider.document` into the `Collaboration` extension and `provider` into `CollaborationCaret` — see the [collaborative editing guide](https://tiptap.dev/docs/hocuspocus/guides/collaborative-editing) for a full example. + +## Exports + +**Components** + +- `HocuspocusProviderWebsocketComponent` — manages the shared WebSocket +- `HocuspocusRoom` — creates a per-document provider on the shared socket; StrictMode-safe + +**Hooks** (must be used inside a `HocuspocusRoom`) + +- `useHocuspocusProvider()` — the `HocuspocusProvider` instance +- `useHocuspocusConnectionStatus()` — `'connecting' | 'connected' | 'disconnected'` +- `useHocuspocusSyncStatus()` — `'synced' | 'syncing'` +- `useHocuspocusAwareness()` — array of connected users' awareness state +- `useHocuspocusEvent(name, handler)` — subscribe to any provider event + +## Documentation + +Full components, hooks, and patterns reference: [tiptap.dev/docs/hocuspocus/provider/react](https://tiptap.dev/docs/hocuspocus/provider/react). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/provider-react/package.json b/packages/provider-react/package.json new file mode 100644 index 0000000..77fd613 --- /dev/null +++ b/packages/provider-react/package.json @@ -0,0 +1,47 @@ +{ + "name": "@hocuspocus/provider-react", + "version": "4.7.0", + "description": "React bindings for Hocuspocus provider", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "websocket", + "provider", + "react", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-provider-react.cjs", + "module": "dist/hocuspocus-provider-react.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-provider-react.esm.js", + "require": "./dist/hocuspocus-provider-react.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@hocuspocus/provider": "workspace:^", + "react": "^18.0.0 || ^19.0.0", + "yjs": "^13.6.8" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "react": "^19.2.4" + }, + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/provider-react/src/HocuspocusProviderWebsocketComponent.tsx b/packages/provider-react/src/HocuspocusProviderWebsocketComponent.tsx new file mode 100644 index 0000000..a506fd4 --- /dev/null +++ b/packages/provider-react/src/HocuspocusProviderWebsocketComponent.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { useEffect, useMemo, useRef } from "react"; + +import { HocuspocusContext } from "./context.ts"; +import type { HocuspocusProviderWebsocketComponentProps } from "./types.ts"; + +/** + * HocuspocusProviderWebsocketComponent manages the WebSocket connection that is shared across all rooms. + * + * This component creates a single WebSocket connection that can be used by multiple + * HocuspocusRoom components, preventing connection overhead when switching between documents. + * + * @example + * ```tsx + * + * + * + * + * + * ``` + */ +export function HocuspocusProviderWebsocketComponent({ + children, + url, + websocketProvider: externalWebsocketProvider, +}: HocuspocusProviderWebsocketComponentProps) { + const websocketRef = useRef(null); + const destroyTimeoutRef = useRef | null>(null); + + // Create WebSocket provider once on mount. + // Safe in StrictMode: useRef persists across double-renders, so the instance + // is created only once. The deferred destruction in the effect below ensures + // the second mount cancels the pending destroy before it fires. + if (!websocketRef.current && !externalWebsocketProvider) { + websocketRef.current = new HocuspocusProviderWebsocket({ + url: url!, + }); + } + + const websocketProvider = + externalWebsocketProvider ?? + (websocketRef.current as HocuspocusProviderWebsocket); + + // Cleanup on unmount with deferred destruction to handle StrictMode double-mount + useEffect(() => { + if (destroyTimeoutRef.current) { + clearTimeout(destroyTimeoutRef.current); + destroyTimeoutRef.current = null; + } + + return () => { + // Only destroy if we created the websocket (not externally provided) + if (!externalWebsocketProvider) { + destroyTimeoutRef.current = setTimeout(() => { + if (websocketRef.current) { + websocketRef.current.destroy(); + websocketRef.current = null; + } + }, 0); + } + }; + }, [externalWebsocketProvider]); + + const contextValue = useMemo( + () => ({ + websocketProvider, + }), + [websocketProvider], + ); + + return ( + + {children} + + ); +} diff --git a/packages/provider-react/src/HocuspocusRoom.tsx b/packages/provider-react/src/HocuspocusRoom.tsx new file mode 100644 index 0000000..f531eb2 --- /dev/null +++ b/packages/provider-react/src/HocuspocusRoom.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; + +import { HocuspocusContext, HocuspocusRoomContext } from "./context.ts"; +import type { HocuspocusProviderEvents, HocuspocusRoomProps } from "./types.ts"; + +/** + * Mapping from HocuspocusRoom `on*` prop names to provider event names. + */ +const EVENT_PROP_MAP: Record = { + onOpen: "open", + onConnect: "connect", + onClose: "close", + onDisconnect: "disconnect", + onStatus: "status", + onSynced: "synced", + onUnsyncedChanges: "unsyncedChanges", + onMessage: "message", + onOutgoingMessage: "outgoingMessage", + onStateless: "stateless", + onAuthenticated: "authenticated", + onAuthenticationFailed: "authenticationFailed", + onAwarenessUpdate: "awarenessUpdate", + onAwarenessChange: "awarenessChange", + onDestroy: "destroy", +}; + +/** + * HocuspocusRoom manages the connection to a specific document. + * + * It uses the shared WebSocket from HocuspocusProviderWebsocketComponent and creates a document-specific + * provider that connects on mount and disconnects on unmount. + * + * This component handles React's StrictMode gracefully by using deferred destruction, + * preventing unnecessary reconnections during development double-mounts. + * + * @example + * ```tsx + * + * console.error(data.reason)} + * > + * + * + * + * ``` + */ +export function HocuspocusRoom({ + children, + name, + document, + token, + sessionAwareness, + flushDelay, + ...eventHandlers +}: HocuspocusRoomProps) { + const hocuspocusContext = useContext(HocuspocusContext); + + if (!hocuspocusContext) { + throw new Error( + "HocuspocusRoom must be used within a HocuspocusProviderWebsocketComponent", + ); + } + + const { websocketProvider } = hocuspocusContext; + + const [provider, setProvider] = useState( + () => + new HocuspocusProvider({ + name, + websocketProvider, + document, + token, + sessionAwareness, + flushDelay, + }), + ); + + const destroyTimeoutRef = useRef | null>(null); + + // Recreate provider when name, document, or token changes. + // Only compare `document`/`token` when the caller actually provided them — + // otherwise the provider's constructor-initialized defaults (auto-created + // Y.Doc, null token) will never match the undefined prop, causing a destroy + // on every render (and in StrictMode, a stray CloseMessage for the current + // doc once the provider is already attached). + // biome-ignore lint/correctness/useExhaustiveDependencies: provider.configuration holds the previous values we compare against — not a reactive dependency + useEffect(() => { + const shouldRecreate = + provider.configuration.name !== name || + (document !== undefined && + provider.configuration.document !== document) || + (token !== undefined && provider.configuration.token !== token) || + provider.configuration.websocketProvider !== websocketProvider; + + if (shouldRecreate) { + provider.destroy(); + setProvider( + new HocuspocusProvider({ + name, + websocketProvider, + document, + token, + sessionAwareness, + flushDelay, + }), + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [name, document, token, websocketProvider]); + + // Attach/detach lifecycle with deferred destruction for StrictMode + useEffect(() => { + if (destroyTimeoutRef.current) { + clearTimeout(destroyTimeoutRef.current); + destroyTimeoutRef.current = null; + } + + provider.attach(); + + return () => { + destroyTimeoutRef.current = setTimeout(() => { + provider.destroy(); + }, 0); + }; + }, [provider]); + + // Wire up on* event handler props with stable refs + const handlersRef = useRef(eventHandlers); + handlersRef.current = eventHandlers; + + useEffect(() => { + const cleanups: (() => void)[] = []; + + for (const [propName, eventName] of Object.entries(EVENT_PROP_MAP)) { + const listener = (...args: unknown[]) => { + const handler = handlersRef.current[ + propName as keyof typeof handlersRef.current + ] as ((...a: unknown[]) => void) | undefined; + handler?.(...args); + }; + provider.on(eventName, listener); + cleanups.push(() => provider.off(eventName, listener)); + } + + return () => { + for (const cleanup of cleanups) { + cleanup(); + } + }; + }, [provider]); + + const contextValue = useMemo( + () => ({ + provider, + }), + [provider], + ); + + return ( + + {children} + + ); +} diff --git a/packages/provider-react/src/context.ts b/packages/provider-react/src/context.ts new file mode 100644 index 0000000..7eb2282 --- /dev/null +++ b/packages/provider-react/src/context.ts @@ -0,0 +1,18 @@ +import { createContext } from "react"; + +import type { + HocuspocusContextValue, + HocuspocusRoomContextValue, +} from "./types.ts"; + +/** + * Context for the WebSocket connection shared across rooms + */ +export const HocuspocusContext = + createContext(null); + +/** + * Context for the room/document provider + */ +export const HocuspocusRoomContext = + createContext(null); diff --git a/packages/provider-react/src/hooks/index.ts b/packages/provider-react/src/hooks/index.ts new file mode 100644 index 0000000..8631bbc --- /dev/null +++ b/packages/provider-react/src/hooks/index.ts @@ -0,0 +1,5 @@ +export { useHocuspocusAwareness } from "./useHocuspocusAwareness.ts"; +export { useHocuspocusConnectionStatus } from "./useHocuspocusConnectionStatus.ts"; +export { useHocuspocusEvent } from "./useHocuspocusEvent.ts"; +export { useHocuspocusProvider } from "./useHocuspocusProvider.ts"; +export { useHocuspocusSyncStatus } from "./useHocuspocusSyncStatus.ts"; diff --git a/packages/provider-react/src/hooks/useHocuspocusAwareness.ts b/packages/provider-react/src/hooks/useHocuspocusAwareness.ts new file mode 100644 index 0000000..5936622 --- /dev/null +++ b/packages/provider-react/src/hooks/useHocuspocusAwareness.ts @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useRef, useSyncExternalStore } from "react"; + +import type { CollabUser } from "../types.ts"; +import { useHocuspocusProvider } from "./useHocuspocusProvider.ts"; + +/** + * Subscribe to the list of connected users in the document. + * + * This hook uses the Yjs awareness protocol to track users currently + * connected to the document. + * + * @returns Array of user objects with their awareness state + * + * @example + * ```tsx + * function UserList() { + * const users = useHocuspocusAwareness() + * + * return ( + *
+ * {users.map(user => ( + *
+ * {user.name?.[0]} + *
+ * ))} + *
+ * ) + * } + * ``` + */ +export function useHocuspocusAwareness(): CollabUser[] { + const provider = useHocuspocusProvider(); + + const versionRef = useRef(0); + const cacheRef = useRef<{ users: CollabUser[]; version: number }>({ + users: [], + version: -1, + }); + + // Reset cache when provider changes so we don't serve stale users + // from a previous provider instance. + useEffect(() => { + versionRef.current++; + cacheRef.current = { users: [], version: -1 }; + }, [provider]); + + const subscribe = useCallback( + (onStoreChange: () => void) => { + const onChange = () => { + versionRef.current++; + onStoreChange(); + }; + provider.awareness?.on("change", onChange); + return () => { + provider.awareness?.off("change", onChange); + }; + }, + [provider], + ); + + const getSnapshot = useCallback(() => { + const awareness = provider.awareness; + if (!awareness) { + return []; + } + + // Return cached value if awareness hasn't changed since last snapshot + if (cacheRef.current.version === versionRef.current) { + return cacheRef.current.users; + } + + const users: CollabUser[] = []; + awareness.getStates().forEach((state, clientId) => { + users.push({ + clientId, + ...state, + }); + }); + + cacheRef.current = { users, version: versionRef.current }; + return users; + }, [provider]); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} diff --git a/packages/provider-react/src/hooks/useHocuspocusConnectionStatus.ts b/packages/provider-react/src/hooks/useHocuspocusConnectionStatus.ts new file mode 100644 index 0000000..50a4432 --- /dev/null +++ b/packages/provider-react/src/hooks/useHocuspocusConnectionStatus.ts @@ -0,0 +1,47 @@ +import { useCallback, useSyncExternalStore } from "react"; + +import type { ConnectionStatus } from "../types.ts"; +import { useHocuspocusProvider } from "./useHocuspocusProvider.ts"; + +/** + * Subscribe to the connection status of the collaboration provider. + * + * This hook uses React's useSyncExternalStore for optimal integration with + * concurrent rendering features. + * + * @returns The current connection status: 'connecting', 'connected', or 'disconnected' + * + * @example + * ```tsx + * function ConnectionIndicator() { + * const status = useHocuspocusConnectionStatus() + * + * return ( + *
+ * {status === 'connected' ? 'Online' : status === 'connecting' ? 'Connecting...' : 'Offline'} + *
+ * ) + * } + * ``` + */ +export function useHocuspocusConnectionStatus(): ConnectionStatus { + const provider = useHocuspocusProvider(); + + const subscribe = useCallback( + (onStoreChange: () => void) => { + provider.on("status", onStoreChange); + return () => { + provider.off("status", onStoreChange); + }; + }, + [provider], + ); + + const getSnapshot = useCallback( + (): ConnectionStatus => + provider.configuration.websocketProvider.status as ConnectionStatus, + [provider], + ); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} diff --git a/packages/provider-react/src/hooks/useHocuspocusEvent.ts b/packages/provider-react/src/hooks/useHocuspocusEvent.ts new file mode 100644 index 0000000..ea0663e --- /dev/null +++ b/packages/provider-react/src/hooks/useHocuspocusEvent.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef } from "react"; + +import type { HocuspocusProviderEvents } from "../types.ts"; +import { useHocuspocusProvider } from "./useHocuspocusProvider.ts"; + +/** + * Subscribe to events from the HocuspocusProvider for the current room. + * + * The handler is stored in a ref so the subscription stays stable even if + * the handler identity changes between renders. + * + * Must be used within a HocuspocusRoom component. + * + * @param event - The event name to subscribe to + * @param handler - Callback invoked when the event fires + * + * @example + * ```tsx + * function AuthGuard() { + * useHocuspocusEvent('authenticationFailed', (data) => { + * console.error('Auth failed:', data.reason) + * redirectToLogin() + * }) + * + * useHocuspocusEvent('close', (data) => { + * console.log('Connection closed', data.event) + * }) + * + * return null + * } + * ``` + */ +export function useHocuspocusEvent( + event: E, + handler: HocuspocusProviderEvents[E] extends undefined + ? () => void + : (data: HocuspocusProviderEvents[E]) => void, +): void { + const provider = useHocuspocusProvider(); + const handlerRef = useRef(handler); + handlerRef.current = handler; + + useEffect(() => { + const listener = (...args: unknown[]) => { + (handlerRef.current as (...a: unknown[]) => void)(...args); + }; + provider.on(event, listener); + return () => { + provider.off(event, listener); + }; + }, [provider, event]); +} diff --git a/packages/provider-react/src/hooks/useHocuspocusProvider.ts b/packages/provider-react/src/hooks/useHocuspocusProvider.ts new file mode 100644 index 0000000..70f335c --- /dev/null +++ b/packages/provider-react/src/hooks/useHocuspocusProvider.ts @@ -0,0 +1,37 @@ +import { useContext } from "react"; + +import { HocuspocusRoomContext } from "../context.ts"; + +/** + * Access the HocuspocusProvider instance for the current room. + * + * Must be used within a HocuspocusRoom component. + * + * @returns The HocuspocusProvider instance + * @throws Error if used outside of HocuspocusRoom + * + * @example + * ```tsx + * function Editor() { + * const provider = useHocuspocusProvider() + * + * const editor = useEditor({ + * extensions: [ + * Collaboration.configure({ document: provider.document }), + * CollaborationCursor.configure({ provider }), + * ], + * }) + * + * return + * } + * ``` + */ +export function useHocuspocusProvider() { + const context = useContext(HocuspocusRoomContext); + + if (!context) { + throw new Error("useHocuspocusProvider must be used within a HocuspocusRoom"); + } + + return context.provider; +} diff --git a/packages/provider-react/src/hooks/useHocuspocusSyncStatus.ts b/packages/provider-react/src/hooks/useHocuspocusSyncStatus.ts new file mode 100644 index 0000000..f48fbf5 --- /dev/null +++ b/packages/provider-react/src/hooks/useHocuspocusSyncStatus.ts @@ -0,0 +1,51 @@ +import { useCallback, useSyncExternalStore } from "react"; + +import type { SyncStatus } from "../types.ts"; +import { useHocuspocusProvider } from "./useHocuspocusProvider.ts"; + +/** + * Subscribe to the sync status of local changes. + * + * This hook indicates whether local changes have been synced with the server. + * + * @returns The current sync status: 'synced' or 'syncing' + * + * @example + * ```tsx + * function SaveIndicator() { + * const syncStatus = useHocuspocusSyncStatus() + * + * return ( + *
+ * {syncStatus === 'syncing' ? 'Saving...' : 'All changes saved'} + *
+ * ) + * } + * ``` + */ +export function useHocuspocusSyncStatus(): SyncStatus { + const provider = useHocuspocusProvider(); + + const subscribe = useCallback( + (onStoreChange: () => void) => { + provider.on("synced", onStoreChange); + provider.on("unsyncedChanges", onStoreChange); + + return () => { + provider.off("synced", onStoreChange); + provider.off("unsyncedChanges", onStoreChange); + }; + }, + [provider], + ); + + const getSnapshot = useCallback((): SyncStatus => { + // Check if there are unsynced changes + if (provider.unsyncedChanges > 0) { + return "syncing"; + } + return "synced"; + }, [provider]); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} diff --git a/packages/provider-react/src/index.ts b/packages/provider-react/src/index.ts new file mode 100644 index 0000000..4cf2e9c --- /dev/null +++ b/packages/provider-react/src/index.ts @@ -0,0 +1,29 @@ +"use client"; + +// Contexts +export { HocuspocusContext, HocuspocusRoomContext } from "./context.ts"; + +// Components +export { HocuspocusProviderWebsocketComponent } from "./HocuspocusProviderWebsocketComponent.tsx"; +export { HocuspocusRoom } from "./HocuspocusRoom.tsx"; + +// Hooks +export { + useHocuspocusAwareness, + useHocuspocusConnectionStatus, + useHocuspocusEvent, + useHocuspocusProvider, + useHocuspocusSyncStatus, +} from "./hooks/index.ts"; + +// Types +export type { + CollabUser, + ConnectionStatus, + HocuspocusContextValue, + HocuspocusProviderEvents, + HocuspocusProviderWebsocketComponentProps, + HocuspocusRoomContextValue, + HocuspocusRoomProps, + SyncStatus, +} from "./types.ts"; diff --git a/packages/provider-react/src/types.ts b/packages/provider-react/src/types.ts new file mode 100644 index 0000000..15b0446 --- /dev/null +++ b/packages/provider-react/src/types.ts @@ -0,0 +1,155 @@ +import type { + HocuspocusProvider, + HocuspocusProviderWebsocket, + onAuthenticatedParameters, + onAuthenticationFailedParameters, + onAwarenessChangeParameters, + onAwarenessUpdateParameters, + onCloseParameters, + onDisconnectParameters, + onMessageParameters, + onOpenParameters, + onOutgoingMessageParameters, + onStatelessParameters, + onStatusParameters, + onSyncedParameters, + onUnsyncedChangesParameters, +} from "@hocuspocus/provider"; +import type { ReactNode } from "react"; +import type * as Y from "yjs"; + +/** + * Configuration for HocuspocusProvider component. + * + * Provide either a `url` to create a managed WebSocket connection, + * or a `websocketProvider` instance for full control — but not both. + */ +export type HocuspocusProviderWebsocketComponentProps = { + children: ReactNode; +} & ( + | { + /** + * URL of your @hocuspocus/server instance + */ + url: string; + websocketProvider?: never; + } + | { + url?: never; + /** + * Provide your own websocket instance for full control + */ + websocketProvider: HocuspocusProviderWebsocket; + } +); + +/** + * Map of HocuspocusProvider event names to their payload types. + * Used by `useHocuspocusEvent` and the `HocuspocusRoom` event handler props. + */ +export interface HocuspocusProviderEvents { + open: onOpenParameters; + connect: undefined; + close: onCloseParameters; + disconnect: onDisconnectParameters; + status: onStatusParameters; + synced: onSyncedParameters; + unsyncedChanges: onUnsyncedChangesParameters; + message: onMessageParameters; + outgoingMessage: onOutgoingMessageParameters; + stateless: onStatelessParameters; + authenticated: onAuthenticatedParameters; + authenticationFailed: onAuthenticationFailedParameters; + awarenessUpdate: onAwarenessUpdateParameters; + awarenessChange: onAwarenessChangeParameters; + destroy: undefined; +} + +/** + * Configuration for HocuspocusRoom component + */ +export interface HocuspocusRoomProps { + children: ReactNode; + /** + * The document name (required) + */ + name: string; + /** + * Optional: bring your own Y.Doc + */ + document?: Y.Doc; + /** + * JWT token or function that returns a promise resolving to a token + */ + token?: string | (() => Promise) | (() => string); + /** + * Enable session-aware multiplexing. When true, the provider embeds a unique + * sessionId in the documentName field of every message, allowing multiple + * providers with the same document name on a single WebSocket connection. + * + * Only set this to `true` when connecting to a v4 server that does + * support session awareness. + * + * Default: false + */ + sessionAwareness?: boolean; + + /** + * Batch outgoing document and awareness updates over a short window (in + * milliseconds) instead of sending one message per change, to reduce the + * number of websocket messages during heavy editing. The added latency is + * capped at `flushDelay`, so keep it small (e.g. 500). + * + * Default: false (send every change immediately) + */ + flushDelay?: false | number; + + // Event handlers — all optional, called when the provider emits the corresponding event. + onOpen?: (data: onOpenParameters) => void; + onConnect?: () => void; + onClose?: (data: onCloseParameters) => void; + onDisconnect?: (data: onDisconnectParameters) => void; + onStatus?: (data: onStatusParameters) => void; + onSynced?: (data: onSyncedParameters) => void; + onUnsyncedChanges?: (data: onUnsyncedChangesParameters) => void; + onMessage?: (data: onMessageParameters) => void; + onOutgoingMessage?: (data: onOutgoingMessageParameters) => void; + onStateless?: (data: onStatelessParameters) => void; + onAuthenticated?: (data: onAuthenticatedParameters) => void; + onAuthenticationFailed?: (data: onAuthenticationFailedParameters) => void; + onAwarenessUpdate?: (data: onAwarenessUpdateParameters) => void; + onAwarenessChange?: (data: onAwarenessChangeParameters) => void; + onDestroy?: () => void; +} + +/** + * Context value for the WebSocket provider + */ +export interface HocuspocusContextValue { + websocketProvider: HocuspocusProviderWebsocket; +} + +/** + * Context value for the room/document provider + */ +export interface HocuspocusRoomContextValue { + provider: HocuspocusProvider; +} + +/** + * Connection status for the collaboration provider + */ +export type ConnectionStatus = "connecting" | "connected" | "disconnected"; + +/** + * Sync status indicating whether local changes are synced with server + */ +export type SyncStatus = "synced" | "syncing"; + +/** + * User information from awareness + */ +export interface CollabUser { + clientId: number; + [key: string]: unknown; +} diff --git a/packages/provider-react/tsconfig.json b/packages/provider-react/tsconfig.json new file mode 100644 index 0000000..88351b8 --- /dev/null +++ b/packages/provider-react/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/packages/provider/CHANGELOG.md b/packages/provider/CHANGELOG.md new file mode 100644 index 0000000..140ea8a --- /dev/null +++ b/packages/provider/CHANGELOG.md @@ -0,0 +1,1492 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + + +### Bug Fixes + +* emit maxAttemptsFailed instead of unhandled rejection ([#1149](https://github.com/ueberdosis/hocuspocus/issues/1149)) ([0218bc0](https://github.com/ueberdosis/hocuspocus/commit/0218bc04aebd050946da2542e8f648ccf4f93d30)) + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* ignore message in awarenessUpdateHandler if origin=this ([#1129](https://github.com/ueberdosis/hocuspocus/issues/1129)) ([08b25d4](https://github.com/ueberdosis/hocuspocus/commit/08b25d4b258d932c68c999c14edcb4efc65c7a9b)) +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + + +### Features + +* add `flushDelay` option for batching updates to reduce websocket traffic during heavy editing ([#1118](https://github.com/ueberdosis/hocuspocus/issues/1118)) ([75594c0](https://github.com/ueberdosis/hocuspocus/commit/75594c05d57d48f2f70d4c9440c28b8226bf95ac)) + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Features + +* first draft of @hocuspocus/provider-react package ([#1059](https://github.com/ueberdosis/hocuspocus/issues/1059)) ([cbc9977](https://github.com/ueberdosis/hocuspocus/commit/cbc99770096e8c674dae304c7e1a16e0fbec827b)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + + +### Bug Fixes + +* turn off sessionAwareness by default to keep BC with old servers ([3a57071](https://github.com/ueberdosis/hocuspocus/commit/3a57071517adb5c9e037fb7c0ac169ffc330df90)) + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + + +### Features + +* adds deduplication for queued awareness messages ([e0453f0](https://github.com/ueberdosis/hocuspocus/commit/e0453f0210a4a30f6f8b742629ed777724429df9)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Features + +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + + +### Features + +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Features + +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + + +### Bug Fixes + +* fixes [#1041](https://github.com/ueberdosis/hocuspocus/issues/1041): websocket URL ending with / wont work ([#1046](https://github.com/ueberdosis/hocuspocus/issues/1046)) ([0df9b09](https://github.com/ueberdosis/hocuspocus/commit/0df9b095d47e7953a9dff090942e937894d8f7fc)) +* fixes provider.forwardClose type error ([5438500](https://github.com/ueberdosis/hocuspocus/commit/543850064ba1cb18c13cb4211cec76a3ad05ddb4)) +* strict types for authorizedScope ([#1029](https://github.com/ueberdosis/hocuspocus/issues/1029)) ([44e1b5d](https://github.com/ueberdosis/hocuspocus/commit/44e1b5d19f632bf646b654efd2c2ba70587adf62)) + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + + +### Features + +* add onTokenSync hook for auth token re-sync ([#1001](https://github.com/ueberdosis/hocuspocus/issues/1001)) ([ee252fb](https://github.com/ueberdosis/hocuspocus/commit/ee252fb99ea6faf80cc71d2e1ce51726e698b76b)) + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Bug Fixes + +* emit status event when status is updated to connected in onOpen, fixes [#1004](https://github.com/ueberdosis/hocuspocus/issues/1004) ([14829c1](https://github.com/ueberdosis/hocuspocus/commit/14829c1ee429359b3648c3021942fd581aa52f4d)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + + +### Features + +* adds configuration option autoConnect (default=true) which allows creating the provider without triggering a connection attempt (fixes [#938](https://github.com/ueberdosis/hocuspocus/issues/938)) ([adc315f](https://github.com/ueberdosis/hocuspocus/commit/adc315f0633f6fdad310384b5217fbe25e29108a)) + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + + +### Bug Fixes + +* race condition when multiple providers connect to the same socket ; fixes [#964](https://github.com/ueberdosis/hocuspocus/issues/964) ([#966](https://github.com/ueberdosis/hocuspocus/issues/966)) ([4868bae](https://github.com/ueberdosis/hocuspocus/commit/4868bae54f7a82c5b0601edc325d42fd1f9420fd)) + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + + +### Features + +* HocuspocusProviderWebsocket: adds handleTimeout callback that is called when the connection doesnt establish within the given timeout ([509e31b](https://github.com/ueberdosis/hocuspocus/commit/509e31bdcc23b625f8245c8c2d2ea320729dd8e7)) + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + + +### Bug Fixes + +* **comments:** fix error where deletedComments yArray does not exist ([#884](https://github.com/ueberdosis/hocuspocus/issues/884)) ([1e405ef](https://github.com/ueberdosis/hocuspocus/commit/1e405ef6da274c75c035131f7a2bf3eebad0fd4d)) + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + + +### Bug Fixes + +* **provider:** allow deletion of first item ([3fe0b91](https://github.com/ueberdosis/hocuspocus/commit/3fe0b91cd5e13b53c6b2ffa9fdf6af3962c26b0b)) +* **provider:** correct event handler binding for document updates and awareness ([e3c616c](https://github.com/ueberdosis/hocuspocus/commit/e3c616c2cd18a717cd3c36a093f4954a7cac39fc)) +* **provider:** fix comment sorting ([e706537](https://github.com/ueberdosis/hocuspocus/commit/e7065378846260ed4ec59c3528a509b8239162c3)) + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + + +### Bug Fixes + +* improve global window checks to enable react native support ([#823](https://github.com/ueberdosis/hocuspocus/issues/823)) ([5dd53d5](https://github.com/ueberdosis/hocuspocus/commit/5dd53d554f5e54984a0743d25dfd16224a95a0d0)) + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + + +### Reverts + +* Revert "websocket: enable following redirects (#806)" (#807) ([930a61c](https://github.com/ueberdosis/hocuspocus/commit/930a61c81e111cda2b692bf1d89b38bfce5d721d)), closes [#806](https://github.com/ueberdosis/hocuspocus/issues/806) [#807](https://github.com/ueberdosis/hocuspocus/issues/807) + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + + +### Bug Fixes + +* allow resolvedAt to be null on updateThread ([eac0bf9](https://github.com/ueberdosis/hocuspocus/commit/eac0bf994d37b072549fe446557efa0c4b2f1568)) + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + + +### Bug Fixes + +* add `documentName` into broadcast message ([#713](https://github.com/ueberdosis/hocuspocus/issues/713)) ([8400928](https://github.com/ueberdosis/hocuspocus/commit/840092861fa2801c62d5c12d5574bfe24e266c3f)) + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + + +### Bug Fixes + +* hocuspocus provider auto connect and disconnect behavior ([#663](https://github.com/ueberdosis/hocuspocus/issues/663)) ([a035c09](https://github.com/ueberdosis/hocuspocus/commit/a035c09b0da4347b0de7cdbf4cfe7f59a14b76f3)) +* remove awareness states on page unload instead of beforeunload ([#658](https://github.com/ueberdosis/hocuspocus/issues/658)) ([e1bd3f1](https://github.com/ueberdosis/hocuspocus/commit/e1bd3f1c9d9486382e1a856faac5118220a39087)) + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + + +### Bug Fixes + +* Do not retry connection if data is too large ([#586](https://github.com/ueberdosis/hocuspocus/issues/586)) ([462a87f](https://github.com/ueberdosis/hocuspocus/commit/462a87f08bfdcaece4adb1ef8dca25ae42b3cb36)) + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Bug Fixes + +* Account for this.webSocket being null ([#235](https://github.com/ueberdosis/hocuspocus/issues/235)) ([b1476b6](https://github.com/ueberdosis/hocuspocus/commit/b1476b60f0eceb83d70c1ded2b3139e4f865a869)) +* Empty sync message causes error in client MessageReceiver ([#174](https://github.com/ueberdosis/hocuspocus/issues/174)) ([f9dca69](https://github.com/ueberdosis/hocuspocus/commit/f9dca69eb96d1ede37a0709bd3b7735bf1ff57ba)) +* Remote client awareness not immediately available ([37703f6](https://github.com/ueberdosis/hocuspocus/commit/37703f695f6bf3b0508c287294c5d26d2e888c37)) + + +### Features + +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.39...@hocuspocus/provider@1.0.0-alpha.40) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.38...@hocuspocus/provider@1.0.0-alpha.39) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.37...@hocuspocus/provider@1.0.0-alpha.38) (2022-06-13) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.36...@hocuspocus/provider@1.0.0-alpha.37) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.35...@hocuspocus/provider@1.0.0-alpha.36) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.34...@hocuspocus/provider@1.0.0-alpha.35) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.34](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.33...@hocuspocus/provider@1.0.0-alpha.34) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.32...@hocuspocus/provider@1.0.0-alpha.33) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.31...@hocuspocus/provider@1.0.0-alpha.32) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.30...@hocuspocus/provider@1.0.0-alpha.31) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.29...@hocuspocus/provider@1.0.0-alpha.30) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.28...@hocuspocus/provider@1.0.0-alpha.29) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.27...@hocuspocus/provider@1.0.0-alpha.28) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.26...@hocuspocus/provider@1.0.0-alpha.27) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.25...@hocuspocus/provider@1.0.0-alpha.26) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.24...@hocuspocus/provider@1.0.0-alpha.25) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.23...@hocuspocus/provider@1.0.0-alpha.24) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.22...@hocuspocus/provider@1.0.0-alpha.23) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.21...@hocuspocus/provider@1.0.0-alpha.22) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.20...@hocuspocus/provider@1.0.0-alpha.21) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.19...@hocuspocus/provider@1.0.0-alpha.20) (2021-10-28) + + +### Bug Fixes + +* Account for this.webSocket being null ([#235](https://github.com/ueberdosis/hocuspocus/issues/235)) ([b1476b6](https://github.com/ueberdosis/hocuspocus/commit/b1476b60f0eceb83d70c1ded2b3139e4f865a869)) + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.18...@hocuspocus/provider@1.0.0-alpha.19) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.17...@hocuspocus/provider@1.0.0-alpha.18) (2021-10-06) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.16...@hocuspocus/provider@1.0.0-alpha.17) (2021-10-05) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.15...@hocuspocus/provider@1.0.0-alpha.16) (2021-09-17) + + +### Bug Fixes + +* Remote client awareness not immediately available ([37703f6](https://github.com/ueberdosis/hocuspocus/commit/37703f695f6bf3b0508c287294c5d26d2e888c37)) + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.14...@hocuspocus/provider@1.0.0-alpha.15) (2021-09-14) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.13...@hocuspocus/provider@1.0.0-alpha.14) (2021-09-03) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.12...@hocuspocus/provider@1.0.0-alpha.13) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.11...@hocuspocus/provider@1.0.0-alpha.12) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.10...@hocuspocus/provider@1.0.0-alpha.11) (2021-08-27) + + +### Bug Fixes + +* Empty sync message causes error in client MessageReceiver ([#174](https://github.com/ueberdosis/hocuspocus/issues/174)) ([f9dca69](https://github.com/ueberdosis/hocuspocus/commit/f9dca69eb96d1ede37a0709bd3b7735bf1ff57ba)) + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.9...@hocuspocus/provider@1.0.0-alpha.10) (2021-08-19) + + +### Features + +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.8...@hocuspocus/provider@1.0.0-alpha.9) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.7...@hocuspocus/provider@1.0.0-alpha.8) (2021-06-23) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.6...@hocuspocus/provider@1.0.0-alpha.7) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.5...@hocuspocus/provider@1.0.0-alpha.6) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.4...@hocuspocus/provider@1.0.0-alpha.5) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.3...@hocuspocus/provider@1.0.0-alpha.4) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.2...@hocuspocus/provider@1.0.0-alpha.3) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/provider + + + + + +# 1.0.0-alpha.2 (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/provider diff --git a/packages/provider/README.md b/packages/provider/README.md new file mode 100644 index 0000000..0244ac0 --- /dev/null +++ b/packages/provider/README.md @@ -0,0 +1,71 @@ +# @hocuspocus/provider +[![Version](https://img.shields.io/npm/v/@hocuspocus/provider.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/provider) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/provider.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/provider.svg)](https://www.npmjs.com/package/@hocuspocus/provider) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +The client-side provider for [Hocuspocus](https://github.com/ueberdosis/hocuspocus). Connects to a Hocuspocus server over WebSockets and syncs one or more [Y.js](https://github.com/yjs/yjs) documents — including awareness (presence), authentication, and stateless messaging. + +> **Building a React app?** Use [`@hocuspocus/provider-react`](../provider-react) instead — it wraps the provider in components and hooks so React handles the lifecycle for you (including StrictMode double-mounts). + +## Installation + +```bash +npm install @hocuspocus/provider yjs +``` + +## Usage + +```js +import * as Y from "yjs" +import { HocuspocusProvider } from "@hocuspocus/provider" + +const ydoc = new Y.Doc() + +const provider = new HocuspocusProvider({ + url: "ws://127.0.0.1:1234", + name: "example-document", + document: ydoc, +}) +``` + +Changes to `ydoc` are now synced to every other client connected to the same `name`. + +### Authenticating + +Pass a `token` — it's forwarded to the server's `onAuthenticate` hook: + +```js +new HocuspocusProvider({ + url: "wss://collab.example.com", + name: "example-document", + document: ydoc, + token: "super-secret-token", +}) +``` + +### Sharing a socket across documents + +Create a `HocuspocusProviderWebsocket` once, then reuse it for multiple documents: + +```js +import { + HocuspocusProvider, + HocuspocusProviderWebsocket, +} from "@hocuspocus/provider" + +const socket = new HocuspocusProviderWebsocket({ url: "ws://127.0.0.1:1234" }) + +const doc1 = new HocuspocusProvider({ websocketProvider: socket, name: "doc-1" }) +const doc2 = new HocuspocusProvider({ websocketProvider: socket, name: "doc-2" }) +``` + +Call `provider.destroy()` to disconnect a single document. Call `socket.destroy()` to tear down the shared connection. + +## Documentation + +Full configuration, events, and awareness reference: [tiptap.dev/docs/hocuspocus/provider](https://tiptap.dev/docs/hocuspocus/provider/overview). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/provider/package.json b/packages/provider/package.json new file mode 100644 index 0000000..a3bb02b --- /dev/null +++ b/packages/provider/package.json @@ -0,0 +1,47 @@ +{ + "name": "@hocuspocus/provider", + "version": "4.7.0", + "description": "hocuspocus provider", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "websocket", + "provider", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-provider.cjs", + "module": "dist/hocuspocus-provider.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-provider.esm.js", + "require": "./dist/hocuspocus-provider.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/common": "workspace:^", + "@lifeomic/attempt": "^3.1.0", + "lib0": "^0.2.117" + }, + "peerDependencies": { + "y-protocols": "^1.0.6", + "yjs": "^13.6.8" + }, + "gitHead": "cd788b6a315f608ef531524409abdce1e6790726", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/provider/src/EventEmitter.ts b/packages/provider/src/EventEmitter.ts new file mode 100644 index 0000000..4585c1b --- /dev/null +++ b/packages/provider/src/EventEmitter.ts @@ -0,0 +1,44 @@ +export default class EventEmitter { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + public callbacks: { [key: string]: Function[] } = {}; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + public on(event: string, fn: Function): this { + if (!this.callbacks[event]) { + this.callbacks[event] = []; + } + + this.callbacks[event].push(fn); + + return this; + } + + protected emit(event: string, ...args: any): this { + const callbacks = this.callbacks[event]; + + if (callbacks) { + callbacks.forEach((callback) => callback.apply(this, args)); + } + + return this; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + public off(event: string, fn?: Function): this { + const callbacks = this.callbacks[event]; + + if (callbacks) { + if (fn) { + this.callbacks[event] = callbacks.filter((callback) => callback !== fn); + } else { + delete this.callbacks[event]; + } + } + + return this; + } + + removeAllListeners(): void { + this.callbacks = {}; + } +} diff --git a/packages/provider/src/HocuspocusProvider.ts b/packages/provider/src/HocuspocusProvider.ts new file mode 100644 index 0000000..e2cf7ba --- /dev/null +++ b/packages/provider/src/HocuspocusProvider.ts @@ -0,0 +1,760 @@ +import { + awarenessStatesToArray, + makeRoutingKey, + parseRoutingKey, +} from "@hocuspocus/common"; +import { Awareness, removeAwarenessStates } from "y-protocols/awareness"; +import * as Y from "yjs"; +import EventEmitter from "./EventEmitter.ts"; +import type { CompleteHocuspocusProviderWebsocketConfiguration } from "./HocuspocusProviderWebsocket.ts"; +import { HocuspocusProviderWebsocket } from "./HocuspocusProviderWebsocket.ts"; +import { IncomingMessage } from "./IncomingMessage.ts"; +import { MessageReceiver } from "./MessageReceiver.ts"; +import { MessageSender } from "./MessageSender.ts"; +import { AuthenticationMessage } from "./OutgoingMessages/AuthenticationMessage.ts"; +import { AwarenessMessage } from "./OutgoingMessages/AwarenessMessage.ts"; +import { StatelessMessage } from "./OutgoingMessages/StatelessMessage.ts"; +import { SyncStepOneMessage } from "./OutgoingMessages/SyncStepOneMessage.ts"; +import { UpdateMessage } from "./OutgoingMessages/UpdateMessage.ts"; +import type { + AuthorizedScope, + ConstructableOutgoingMessage, + onAuthenticatedParameters, + onAuthenticationFailedParameters, + onAwarenessChangeParameters, + onAwarenessUpdateParameters, + onCloseParameters, + onDisconnectParameters, + onMessageParameters, + onOpenParameters, + onOutgoingMessageParameters, + onStatelessParameters, + onStatusParameters, + onSyncedParameters, + onUnsyncedChangesParameters, +} from "./types.ts"; + +export type HocuspocusProviderConfiguration = Required< + Pick +> & + Partial & + ( + | (Required> & + Partial< + Pick< + CompleteHocuspocusProviderWebsocketConfiguration, + "preserveTrailingSlash" + > + >) + | Required< + Pick + > + ); + +export interface CompleteHocuspocusProviderConfiguration { + /** + * The identifier/name of your document + */ + name: string; + /** + * The actual Y.js document + */ + document: Y.Doc; + + /** + * An Awareness instance to keep the presence state of all clients. + * + * You can disable sharing awareness information by passing `null`. + * Note that having no awareness information shared across all connections will break our ping checks + * and thus trigger reconnects. You should always have at least one Provider with enabled awareness per + * socket connection, or ensure that the Provider receives messages before running into `HocuspocusProviderWebsocket.messageReconnectTimeout`. + */ + awareness: Awareness | null; + + /** + * A token that’s sent to the backend for authentication purposes. + */ + token: string | (() => string) | (() => Promise) | null; + + /** + * Hocuspocus websocket provider + */ + websocketProvider: HocuspocusProviderWebsocket; + + /** + * Enable session-aware multiplexing. When true, the provider embeds a unique + * sessionId in the documentName field of every message, allowing multiple + * providers with the same document name on a single WebSocket connection. + * + * Only set this to `true` when connecting to a v4 server that does + * support session awareness. + * + * Default: false + */ + sessionAwareness: boolean; + + /** + * Force syncing the document in the defined interval. + */ + forceSyncInterval: false | number; + + /** + * Batch outgoing document and awareness updates over a short window (in + * milliseconds) instead of sending one message per change. During heavy + * editing this drastically reduces the number of websocket messages: Yjs + * updates collected in the window are merged with `Y.mergeUpdates` into a + * single message, and awareness collapses to the latest state of each + * changed client. + * + * The window is a fixed batch, not a resetting debounce, so the added + * latency is capped at `flushDelay` even while the user keeps typing. Keep + * it small (e.g. 500) to avoid delaying what other clients see. + * + * Set to `false` (the default) to send every change immediately. + */ + flushDelay: false | number; + + onAuthenticated: (data: onAuthenticatedParameters) => void; + onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void; + onOpen: (data: onOpenParameters) => void; + onConnect: () => void; + onStatus: (data: onStatusParameters) => void; + onMessage: (data: onMessageParameters) => void; + onOutgoingMessage: (data: onOutgoingMessageParameters) => void; + onSynced: (data: onSyncedParameters) => void; + onDisconnect: (data: onDisconnectParameters) => void; + onClose: (data: onCloseParameters) => void; + onDestroy: () => void; + onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void; + onAwarenessChange: (data: onAwarenessChangeParameters) => void; + onStateless: (data: onStatelessParameters) => void; + onUnsyncedChanges: (data: onUnsyncedChangesParameters) => void; +} + +export class AwarenessError extends Error { + code = 1001; +} + +export class HocuspocusProvider extends EventEmitter { + public configuration: CompleteHocuspocusProviderConfiguration = { + name: "", + // @ts-expect-error + document: undefined, + // @ts-expect-error + awareness: undefined, + token: null, + sessionAwareness: false, + forceSyncInterval: false, + flushDelay: false, + onAuthenticated: () => null, + onAuthenticationFailed: () => null, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onSynced: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + onStateless: () => null, + onUnsyncedChanges: () => null, + }; + + isSynced = false; + + unsyncedChanges = 0; + + isAuthenticated = false; + + authorizedScope: AuthorizedScope | undefined = undefined; + + // @internal + manageSocket = false; + + private _isAttached = false; + + /** + * Outgoing document updates buffered for the current `flushDelay` window. + */ + private pendingUpdates: Uint8Array[] = []; + + /** + * Awareness client ids whose latest state should be sent on the next flush. + */ + private pendingAwarenessClients = new Set(); + + private flushTimeout: ReturnType | null = null; + + /** + * Unique session identifier for this provider instance. + * Used for multiplexing multiple providers with the same document name on a single WebSocket. + */ + sessionId: string = Math.random().toString(36).slice(2); + + /** + * The effective name used as the first VarString in messages. + * When `sessionAwareness` is enabled, returns a composite key (documentName\0sessionId). + * Otherwise, returns the plain document name. + */ + get effectiveName(): string { + return this.configuration.sessionAwareness + ? makeRoutingKey(this.configuration.name, this.sessionId) + : this.configuration.name; + } + + intervals: any = { + forceSync: null, + }; + + constructor(configuration: HocuspocusProviderConfiguration) { + super(); + this.setConfiguration(configuration); + + this.configuration.document = configuration.document + ? configuration.document + : new Y.Doc(); + this.configuration.awareness = + configuration.awareness !== undefined + ? configuration.awareness + : new Awareness(this.document); + + this.on("open", this.configuration.onOpen); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("synced", this.configuration.onSynced); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + this.on("stateless", this.configuration.onStateless); + this.on("unsyncedChanges", this.configuration.onUnsyncedChanges); + + this.on("authenticated", this.configuration.onAuthenticated); + this.on("authenticationFailed", this.configuration.onAuthenticationFailed); + + this.awareness?.on("update", () => { + this.emit("awarenessUpdate", { + states: awarenessStatesToArray(this.awareness!.getStates()), + }); + }); + + this.awareness?.on("change", () => { + this.emit("awarenessChange", { + states: awarenessStatesToArray(this.awareness!.getStates()), + }); + }); + + this.document.on("update", this.boundDocumentUpdateHandler); + this.awareness?.on("update", this.boundAwarenessUpdateHandler); + + this.registerEventListeners(); + + if ( + this.configuration.forceSyncInterval && + typeof this.configuration.forceSyncInterval === "number" + ) { + this.intervals.forceSync = setInterval( + this.forceSync.bind(this), + this.configuration.forceSyncInterval, + ); + } + + if (this.manageSocket) { + this.attach(); + } + } + + boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this); + + boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this); + + boundPageHide = this.pageHide.bind(this); + + boundOnOpen = this.onOpen.bind(this); + + boundOnClose = this.onClose.bind(this); + + forwardConnect = () => this.emit("connect"); + + forwardStatus = (e: onStatusParameters) => this.emit("status", e); + + forwardClose = (e: onCloseParameters) => this.emit("close", e); + + forwardDisconnect = (e: onDisconnectParameters) => this.emit("disconnect", e); + + forwardDestroy = () => this.emit("destroy"); + + public setConfiguration( + configuration: Partial = {}, + ): void { + if (!configuration.websocketProvider) { + this.manageSocket = true; + this.configuration.websocketProvider = new HocuspocusProviderWebsocket( + configuration as CompleteHocuspocusProviderWebsocketConfiguration, + ); + } + + this.configuration = { ...this.configuration, ...configuration }; + } + + get document() { + return this.configuration.document; + } + + public get isAttached() { + return this._isAttached; + } + + get awareness() { + return this.configuration.awareness; + } + + get hasUnsyncedChanges(): boolean { + return this.unsyncedChanges > 0; + } + + private resetUnsyncedChanges() { + this.unsyncedChanges = 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + + incrementUnsyncedChanges() { + this.unsyncedChanges += 1; + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + + decrementUnsyncedChanges() { + if (this.unsyncedChanges > 0) { + this.unsyncedChanges -= 1; + } + + if (this.unsyncedChanges === 0) { + this.synced = true; + } + + this.emit("unsyncedChanges", { number: this.unsyncedChanges }); + } + + forceSync() { + this.resetUnsyncedChanges(); + + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.effectiveName, + }); + } + + pageHide() { + if (this.awareness) { + removeAwarenessStates( + this.awareness, + [this.document.clientID], + "page hide", + ); + } + } + + registerEventListeners() { + if (typeof window === "undefined" || !("addEventListener" in window)) { + return; + } + + window.addEventListener("pagehide", this.boundPageHide); + } + + sendStateless(payload: string) { + this.send(StatelessMessage, { + documentName: this.effectiveName, + payload, + }); + } + + async sendToken() { + let token: string | null; + try { + token = await this.getToken(); + } catch (error) { + this.permissionDeniedHandler( + `Failed to get token during sendToken(): ${error}`, + ); + return; + } + + this.send(AuthenticationMessage, { + token: token ?? "", + documentName: this.effectiveName, + }); + } + + private get batchingEnabled(): boolean { + return ( + !!this.configuration.flushDelay && + typeof this.configuration.flushDelay === "number" + ); + } + + documentUpdateHandler(update: Uint8Array, origin: any) { + if (origin === this) { + return; + } + + if (!this.batchingEnabled) { + this.incrementUnsyncedChanges(); + this.send(UpdateMessage, { update, documentName: this.effectiveName }); + return; + } + + // Count one outstanding change per batch: the server acks once per + // merged message, so incrementing per buffered update would never balance. + if (this.pendingUpdates.length === 0) { + this.incrementUnsyncedChanges(); + } + + this.pendingUpdates.push(update); + this.scheduleFlush(); + } + + awarenessUpdateHandler({ added, updated, removed }: any, origin: any) { + if (origin === this) { + return; + } + + const changedClients = added.concat(updated).concat(removed); + + if (!this.batchingEnabled) { + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: changedClients, + documentName: this.effectiveName, + }); + return; + } + + for (const client of changedClients) { + this.pendingAwarenessClients.add(client); + } + + this.scheduleFlush(); + } + + private scheduleFlush() { + // Fixed-window batching: the first pending change starts the timer and + // everything until it fires is flushed together, so latency stays capped + // at `flushDelay` instead of growing while the user keeps typing. + if (this.flushTimeout !== null) { + return; + } + + this.flushTimeout = setTimeout(() => { + this.flushTimeout = null; + this.flushPendingUpdates(); + }, this.configuration.flushDelay as number); + } + + /** + * Send everything buffered for the current `flushDelay` window right away. + * Buffered document updates are merged into a single message and awareness + * collapses to the latest state of each changed client. Safe to call when + * nothing is pending. + */ + flushPendingUpdates() { + if (this.flushTimeout !== null) { + clearTimeout(this.flushTimeout); + this.flushTimeout = null; + } + + if (this.pendingUpdates.length > 0) { + const update = + this.pendingUpdates.length === 1 + ? this.pendingUpdates[0] + : Y.mergeUpdates(this.pendingUpdates); + + this.pendingUpdates = []; + this.send(UpdateMessage, { update, documentName: this.effectiveName }); + } + + if (this.pendingAwarenessClients.size > 0) { + const clients = Array.from(this.pendingAwarenessClients); + + this.pendingAwarenessClients.clear(); + this.send(AwarenessMessage, { + awareness: this.awareness, + clients, + documentName: this.effectiveName, + }); + } + } + + /** + * Indicates whether a first handshake with the server has been established + * + * Note: this does not mean all updates from the client have been persisted to the backend. For this, + * use `hasUnsyncedChanges`. + */ + get synced(): boolean { + return this.isSynced; + } + + set synced(state) { + if (this.isSynced === state) { + return; + } + + this.isSynced = state; + + if (state) { + this.emit("synced", { state }); + } + } + + receiveStateless(payload: string) { + this.emit("stateless", { payload }); + } + + // not needed, but provides backward compatibility with e.g. lexical/yjs + async connect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.connect(); + } + + console.warn( + "HocuspocusProvider::connect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.", + ); + } + + disconnect() { + if (this.manageSocket) { + return this.configuration.websocketProvider.disconnect(); + } + + console.warn( + "HocuspocusProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.", + ); + } + + async onOpen(event: Event) { + this.isAuthenticated = false; + + this.emit("open", { event }); + await this.sendToken(); + this.startSync(); + } + + async getToken() { + if (typeof this.configuration.token === "function") { + const token = await this.configuration.token(); + return token; + } + + return this.configuration.token; + } + + startSync() { + this.resetUnsyncedChanges(); + + this.send(SyncStepOneMessage, { + document: this.document, + documentName: this.effectiveName, + }); + + if (this.awareness && this.awareness.getLocalState() !== null) { + this.send(AwarenessMessage, { + awareness: this.awareness, + clients: [this.document.clientID], + documentName: this.effectiveName, + }); + } + } + + send(message: ConstructableOutgoingMessage, args: any) { + if (!this._isAttached) return; + + const messageSender = new MessageSender(message, args); + + this.emit("outgoingMessage", { message: messageSender.message }); + messageSender.send(this.configuration.websocketProvider); + } + + onMessage(event: MessageEvent) { + const message = new IncomingMessage(event.data); + + const rawKey = message.readVarString(); + // Extract actual documentName from potentially composite routing key + const { documentName } = parseRoutingKey(rawKey); + + message.writeVarString(this.effectiveName); + + this.emit("message", { event, message: new IncomingMessage(event.data) }); + + new MessageReceiver(message).apply(this, true); + } + + onClose() { + this.isAuthenticated = false; + this.synced = false; + + // Drop anything buffered for batching; the reconnect sync handshake will + // reconcile these changes from the document via state vectors. + if (this.flushTimeout !== null) { + clearTimeout(this.flushTimeout); + this.flushTimeout = null; + } + this.pendingUpdates = []; + this.pendingAwarenessClients.clear(); + + // update awareness (all users except local left) + if (this.awareness) { + removeAwarenessStates( + this.awareness, + Array.from(this.awareness.getStates().keys()).filter( + (client) => client !== this.document.clientID, + ), + this, + ); + } + } + + destroy() { + this.emit("destroy"); + + if (this.intervals.forceSync) { + clearInterval(this.intervals.forceSync); + } + + if (this.awareness) { + removeAwarenessStates( + this.awareness, + [this.document.clientID], + "provider destroy", + ); + this.awareness.off("update", this.boundAwarenessUpdateHandler); + this.awareness.destroy(); + } + + // Send any buffered updates before the socket is torn down. The websocket + // safely queues the message if it is no longer open. + this.flushPendingUpdates(); + + this.document.off("update", this.boundDocumentUpdateHandler); + + this.removeAllListeners(); + + this.detach(); + + if (this.manageSocket) { + this.configuration.websocketProvider.destroy(); + } + + if (typeof window === "undefined" || !("removeEventListener" in window)) { + return; + } + + window.removeEventListener("pagehide", this.boundPageHide); + } + + detach() { + this.configuration.websocketProvider.off( + "connect", + this.configuration.onConnect, + ); + this.configuration.websocketProvider.off("connect", this.forwardConnect); + + this.configuration.websocketProvider.off("status", this.forwardStatus); + this.configuration.websocketProvider.off( + "status", + this.configuration.onStatus, + ); + + this.configuration.websocketProvider.off("open", this.boundOnOpen); + this.configuration.websocketProvider.off("close", this.boundOnClose); + this.configuration.websocketProvider.off( + "close", + this.configuration.onClose, + ); + this.configuration.websocketProvider.off("close", this.forwardClose); + this.configuration.websocketProvider.off( + "disconnect", + this.configuration.onDisconnect, + ); + this.configuration.websocketProvider.off( + "disconnect", + this.forwardDisconnect, + ); + this.configuration.websocketProvider.off( + "destroy", + this.configuration.onDestroy, + ); + this.configuration.websocketProvider.off("destroy", this.forwardDestroy); + + this.configuration.websocketProvider.detach(this); + + this._isAttached = false; + } + + attach() { + if (this._isAttached) return; + + this.configuration.websocketProvider.on( + "connect", + this.configuration.onConnect, + ); + this.configuration.websocketProvider.on("connect", this.forwardConnect); + + this.configuration.websocketProvider.on( + "status", + this.configuration.onStatus, + ); + this.configuration.websocketProvider.on("status", this.forwardStatus); + + this.configuration.websocketProvider.on("open", this.boundOnOpen); + + this.configuration.websocketProvider.on("close", this.boundOnClose); + this.configuration.websocketProvider.on( + "close", + this.configuration.onClose, + ); + this.configuration.websocketProvider.on("close", this.forwardClose); + + this.configuration.websocketProvider.on( + "disconnect", + this.configuration.onDisconnect, + ); + this.configuration.websocketProvider.on( + "disconnect", + this.forwardDisconnect, + ); + + this.configuration.websocketProvider.on( + "destroy", + this.configuration.onDestroy, + ); + this.configuration.websocketProvider.on("destroy", this.forwardDestroy); + + this.configuration.websocketProvider.attach(this); + + this._isAttached = true; + } + + permissionDeniedHandler(reason: string) { + this.emit("authenticationFailed", { reason }); + this.isAuthenticated = false; + } + + authenticatedHandler(scope: string) { + this.isAuthenticated = true; + this.authorizedScope = scope as AuthorizedScope; + + this.emit("authenticated", { scope }); + } + + setAwarenessField(key: string, value: any) { + if (!this.awareness) { + throw new AwarenessError( + `Cannot set awareness field "${key}" to ${JSON.stringify(value)}. You have disabled Awareness for this provider by explicitly passing awareness: null in the provider configuration.`, + ); + } + this.awareness.setLocalStateField(key, value); + } +} diff --git a/packages/provider/src/HocuspocusProviderWebsocket.ts b/packages/provider/src/HocuspocusProviderWebsocket.ts new file mode 100644 index 0000000..ba06a9b --- /dev/null +++ b/packages/provider/src/HocuspocusProviderWebsocket.ts @@ -0,0 +1,610 @@ +import { WsReadyStates } from "@hocuspocus/common"; +import { retry } from "@lifeomic/attempt"; +import { createDecoder, readVarString, readVarUint } from "lib0/decoding"; +import * as encoding from "lib0/encoding"; +import * as time from "lib0/time"; +import EventEmitter from "./EventEmitter.ts"; +import type { HocuspocusProvider } from "./HocuspocusProvider.ts"; +import { IncomingMessage } from "./IncomingMessage.ts"; +import { CloseMessage } from "./OutgoingMessages/CloseMessage.ts"; +import { + MessageType, + type onAwarenessChangeParameters, + type onAwarenessUpdateParameters, + type onCloseParameters, + type onDisconnectParameters, + type onMaxAttemptsFailedParameters, + type onMessageParameters, + type onOpenParameters, + type onOutgoingMessageParameters, + type onStatusParameters, + WebSocketStatus, +} from "./types.ts"; + +export type HocuspocusWebSocket = WebSocket & { identifier: string }; +export type HocusPocusWebSocket = HocuspocusWebSocket; + +export type HocuspocusProviderWebsocketConfiguration = Required< + Pick +> & + Partial; + +export interface CompleteHocuspocusProviderWebsocketConfiguration { + /** + * Whether to connect automatically when creating the provider instance. Default=true + */ + autoConnect: boolean; + + /** + * URL of your @hocuspocus/server instance + */ + url: string; + + /** + * By default, trailing slashes are removed from the URL. Set this to true + * to preserve trailing slashes if your server configuration requires them. + */ + preserveTrailingSlash: boolean; + + /** + * An optional WebSocket polyfill, for example for Node.js + */ + WebSocketPolyfill: any; + + /** + * Disconnect when no message is received for the defined amount of milliseconds. + */ + messageReconnectTimeout: number; + /** + * The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially. + */ + delay: number; + /** + * The initialDelay is the amount of time to wait before making the first attempt. This option should typically be 0 since you typically want the first attempt to happen immediately. + */ + initialDelay: number; + /** + * The factor option is used to grow the delay exponentially. + */ + factor: number; + /** + * The maximum number of attempts or 0 if there is no limit on number of attempts. + */ + maxAttempts: number; + /** + * minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled. + */ + minDelay: number; + /** + * The maxDelay option is used to set an upper bound for the delay when factor is enabled. A value of 0 can be provided if there should be no upper bound when calculating delay. + */ + maxDelay: number; + /** + * If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration. + */ + jitter: boolean; + /** + * A timeout in milliseconds. If timeout is non-zero then a timer is set using setTimeout. If the timeout is triggered then future attempts will be aborted. + */ + timeout: number; + handleTimeout: (() => Promise) | null; + onOpen: (data: onOpenParameters) => void; + onConnect: () => void; + onMessage: (data: onMessageParameters) => void; + onOutgoingMessage: (data: onOutgoingMessageParameters) => void; + onStatus: (data: onStatusParameters) => void; + onDisconnect: (data: onDisconnectParameters) => void; + onClose: (data: onCloseParameters) => void; + onMaxAttemptsFailed: (data: onMaxAttemptsFailedParameters) => void; + onDestroy: () => void; + onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void; + onAwarenessChange: (data: onAwarenessChangeParameters) => void; + + /** + * Map of attached providers keyed by documentName. + */ + providerMap: Map; +} + +export class HocuspocusProviderWebsocket extends EventEmitter { + private static readonly DEDUPLICATABLE_TYPES = new Set([ + MessageType.Awareness, + MessageType.QueryAwareness, + ]); + + private messageQueue: any[] = []; + + public configuration: CompleteHocuspocusProviderWebsocketConfiguration = { + url: "", + autoConnect: true, + preserveTrailingSlash: false, + // @ts-expect-error + document: undefined, + WebSocketPolyfill: undefined, + // TODO: this should depend on awareness.outdatedTime + messageReconnectTimeout: 30000, + // 1 second + delay: 1000, + // instant + initialDelay: 0, + // double the delay each time + factor: 2, + // unlimited retries + maxAttempts: 0, + // wait at least 1 second + minDelay: 1000, + // at least every 30 seconds + maxDelay: 30000, + // randomize + jitter: true, + // retry forever + timeout: 0, + onOpen: () => null, + onConnect: () => null, + onMessage: () => null, + onOutgoingMessage: () => null, + onStatus: () => null, + onDisconnect: () => null, + onClose: () => null, + onMaxAttemptsFailed: () => null, + onDestroy: () => null, + onAwarenessUpdate: () => null, + onAwarenessChange: () => null, + handleTimeout: null, + providerMap: new Map(), + }; + + webSocket: HocusPocusWebSocket | null = null; + + webSocketHandlers: { [key: string]: any } = {}; + + shouldConnect = true; + + status = WebSocketStatus.Disconnected; + + lastMessageReceived = 0; + + identifier = 0; + + intervals: any = { + connectionChecker: null, + }; + + connectionAttempt: { + resolve: (value?: any) => void; + reject: (reason?: any) => void; + } | null = null; + + constructor(configuration: HocuspocusProviderWebsocketConfiguration) { + super(); + this.setConfiguration(configuration); + + this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill + ? configuration.WebSocketPolyfill + : WebSocket; + + this.on("open", this.configuration.onOpen); + this.on("open", this.onOpen.bind(this)); + this.on("connect", this.configuration.onConnect); + this.on("message", this.configuration.onMessage); + this.on("outgoingMessage", this.configuration.onOutgoingMessage); + this.on("status", this.configuration.onStatus); + this.on("disconnect", this.configuration.onDisconnect); + this.on("close", this.configuration.onClose); + this.on("maxAttemptsFailed", this.configuration.onMaxAttemptsFailed); + this.on("destroy", this.configuration.onDestroy); + this.on("awarenessUpdate", this.configuration.onAwarenessUpdate); + this.on("awarenessChange", this.configuration.onAwarenessChange); + + this.on("close", this.onClose.bind(this)); + this.on("message", this.onMessage.bind(this)); + + this.intervals.connectionChecker = setInterval( + this.checkConnection.bind(this), + this.configuration.messageReconnectTimeout / 10, + ); + + if (this.shouldConnect) { + this.connect(); + } + } + + receivedOnOpenPayload?: Event | undefined = undefined; + + async onOpen(event: Event) { + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + + this.cancelWebsocketRetry = undefined; + this.receivedOnOpenPayload = event; + } + + attach(provider: HocuspocusProvider) { + const key = provider.effectiveName; + const existing = this.configuration.providerMap.get(key); + + if (existing && existing !== provider) { + // Allow replacing a provider that hasn't authenticated (e.g., after auth failure retry) + if (existing.isAuthenticated) { + throw new Error( + `Cannot attach two providers with the same effective name "${key}". ` + + "Use sessionAwareness: true to multiplex providers with the same document name.", + ); + } + } + + this.configuration.providerMap.set(key, provider); + + if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) { + this.connect(); + } + + if ( + this.receivedOnOpenPayload && + this.status === WebSocketStatus.Connected + ) { + provider.onOpen(this.receivedOnOpenPayload); + } + } + + detach(provider: HocuspocusProvider) { + const key = provider.effectiveName; + if (this.configuration.providerMap.has(key)) { + provider.send(CloseMessage, { + documentName: key, + }); + this.configuration.providerMap.delete(key); + } + } + + public setConfiguration( + configuration: Partial = {}, + ): void { + this.configuration = { ...this.configuration, ...configuration }; + + if (!this.configuration.autoConnect) { + this.shouldConnect = false; + } + } + + cancelWebsocketRetry?: () => void; + + async connect() { + if (this.status === WebSocketStatus.Connected) { + return; + } + + // Always cancel any previously initiated connection retryer instances + if (this.cancelWebsocketRetry) { + this.cancelWebsocketRetry(); + this.cancelWebsocketRetry = undefined; + } + + this.receivedOnOpenPayload = undefined; + this.shouldConnect = true; + + const abortableRetry = () => { + let cancelAttempt = false; + + const retryPromise = retry(this.createWebSocketConnection.bind(this), { + delay: this.configuration.delay, + initialDelay: this.configuration.initialDelay, + factor: this.configuration.factor, + maxAttempts: this.configuration.maxAttempts, + minDelay: this.configuration.minDelay, + maxDelay: this.configuration.maxDelay, + jitter: this.configuration.jitter, + timeout: this.configuration.timeout, + handleTimeout: this.configuration.handleTimeout, + beforeAttempt: (context) => { + if (!this.shouldConnect || cancelAttempt) { + context.abort(); + } + }, + }).catch((error: any) => { + // If we aborted the connection attempt then don’t report a failure + // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136 + if (error?.code !== "ATTEMPT_ABORTED") { + // connect() is fire-and-forget; rethrowing becomes an unhandled rejection. + this.emit("maxAttemptsFailed", { error }); + } + }); + + return { + retryPromise, + cancelFunc: () => { + cancelAttempt = true; + }, + }; + }; + + const { retryPromise, cancelFunc } = abortableRetry(); + this.cancelWebsocketRetry = cancelFunc; + + return retryPromise; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + attachWebSocketListeners(ws: HocusPocusWebSocket, reject: Function) { + const { identifier } = ws; + const onMessageHandler = (payload: any) => this.emit("message", payload); + const onCloseHandler = (payload: any) => + this.emit("close", { event: payload }); + const onOpenHandler = (payload: any) => this.emit("open", payload); + const onErrorHandler = (err: any) => { + reject(err); + }; + + this.webSocketHandlers[identifier] = { + message: onMessageHandler, + close: onCloseHandler, + open: onOpenHandler, + error: onErrorHandler, + }; + + const handlers = this.webSocketHandlers[ws.identifier]; + + Object.keys(handlers).forEach((name) => { + ws.addEventListener(name, handlers[name]); + }); + } + + cleanupWebSocket() { + if (!this.webSocket) { + return; + } + const { identifier } = this.webSocket; + const handlers = this.webSocketHandlers[identifier]; + + Object.keys(handlers).forEach((name) => { + this.webSocket?.removeEventListener(name, handlers[name]); + delete this.webSocketHandlers[identifier]; + }); + this.webSocket.close(); + this.webSocket = null; + } + + createWebSocketConnection() { + return new Promise((resolve, reject) => { + if (this.webSocket) { + this.messageQueue = []; + this.cleanupWebSocket(); + } + this.lastMessageReceived = 0; + this.identifier += 1; + + // Init the WebSocket connection + const ws = new this.configuration.WebSocketPolyfill(this.url); + ws.binaryType = "arraybuffer"; + ws.identifier = this.identifier; + + this.attachWebSocketListeners(ws, reject); + + this.webSocket = ws; + + // Reset the status + this.status = WebSocketStatus.Connecting; + this.emit("status", { status: WebSocketStatus.Connecting }); + + // Store resolve/reject for later use + this.connectionAttempt = { + resolve, + reject, + }; + }); + } + + onMessage(event: MessageEvent) { + this.resolveConnectionAttempt(); + + this.lastMessageReceived = time.getUnixTime(); + + const data = new Uint8Array(event.data as ArrayBuffer); + + // Check for connection-level Ping message (no document name prefix) + // Ping messages are sent as just the message type byte (length 1) + // We check length to avoid confusing with regular messages that happen to have + // a document name length of 9 as the first byte + if (data.length === 1 && data[0] === MessageType.Ping) { + this.sendPong(); + return; + } + + const message = new IncomingMessage(data); + const rawKey = message.peekVarString(); + + const provider = this.configuration.providerMap.get(rawKey); + provider?.onMessage(event); + } + + /** + * Send application-level Pong response to server Ping + */ + private sendPong() { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, MessageType.Pong); + this.send(encoding.toUint8Array(encoder)); + } + + resolveConnectionAttempt() { + if (this.connectionAttempt) { + this.connectionAttempt.resolve(); + this.connectionAttempt = null; + + this.status = WebSocketStatus.Connected; + this.emit("status", { status: WebSocketStatus.Connected }); + this.emit("connect"); + this.messageQueue.forEach((message) => this.send(message)); + this.messageQueue = []; + } + } + + stopConnectionAttempt() { + this.connectionAttempt = null; + } + + rejectConnectionAttempt(reason?: unknown) { + this.connectionAttempt?.reject(reason); + this.connectionAttempt = null; + } + + closeTries = 0; + + checkConnection() { + // Don’t check the connection when it’s not even established + if (this.status !== WebSocketStatus.Connected) { + return; + } + + // Don’t close the connection while waiting for the first message + if (!this.lastMessageReceived) { + return; + } + + // Don’t close the connection when a message was received recently + if ( + this.configuration.messageReconnectTimeout >= + time.getUnixTime() - this.lastMessageReceived + ) { + return; + } + + // No message received in a long time, not even your own + // Awareness updates, which are updated every 15 seconds + // if awareness is enabled. + this.closeTries += 1; + // https://bugs.webkit.org/show_bug.cgi?id=247943 + if (this.closeTries > 2) { + this.onClose({ + event: { + code: 4408, + reason: "forced", + }, + }); + this.closeTries = 0; + } else { + this.webSocket?.close(); + this.messageQueue = []; + } + } + + get serverUrl() { + if (this.configuration.preserveTrailingSlash) { + return this.configuration.url; + } + + // By default, ensure that the URL never ends with / + let url = this.configuration.url; + while (url[url.length - 1] === "/") { + url = url.slice(0, url.length - 1); + } + + return url; + } + + get url() { + return this.serverUrl; + } + + disconnect() { + this.shouldConnect = false; + + if (this.webSocket === null) { + return; + } + + try { + this.webSocket.close(); + this.messageQueue = []; + } catch (e) { + console.error(e); + } + } + + private parseQueuedMessage( + message: Uint8Array, + ): { documentName: string; messageType: number } | null { + try { + const decoder = createDecoder(message); + const documentName = readVarString(decoder); + const messageType = readVarUint(decoder); + return { documentName, messageType }; + } catch { + return null; + } + } + + private addToQueue(message: any) { + if (message instanceof Uint8Array) { + const parsed = this.parseQueuedMessage(message); + if ( + parsed && + HocuspocusProviderWebsocket.DEDUPLICATABLE_TYPES.has(parsed.messageType) + ) { + this.messageQueue = this.messageQueue.filter((queued) => { + if (!(queued instanceof Uint8Array)) return true; + const queuedParsed = this.parseQueuedMessage(queued); + if (!queuedParsed) return true; + return !( + queuedParsed.documentName === parsed.documentName && + queuedParsed.messageType === parsed.messageType + ); + }); + } + } + this.messageQueue.push(message); + } + + send(message: any) { + if (this.webSocket?.readyState === WsReadyStates.Open) { + this.webSocket.send(message); + } else { + this.addToQueue(message); + } + } + + onClose({ event }: onCloseParameters) { + this.closeTries = 0; + this.cleanupWebSocket(); + + if (this.connectionAttempt) { + // That connection attempt failed. The socket can close without ever + // emitting an error, so pass a reason along – it ends up as the error + // of the `maxAttemptsFailed` event once the retries are exhausted. + this.rejectConnectionAttempt( + new Error( + `WebSocket closed before the connection was established (code: ${event?.code}, reason: ${event?.reason || "none"})`, + ), + ); + } + + // Let’s update the connection status. + this.status = WebSocketStatus.Disconnected; + this.emit("status", { status: WebSocketStatus.Disconnected }); + this.emit("disconnect", { event }); + + // trigger connect if no retry is running and we want to have a connection + if (!this.cancelWebsocketRetry && this.shouldConnect) { + setTimeout(() => { + this.connect(); + }, this.configuration.delay); + } + } + + destroy() { + this.emit("destroy"); + + clearInterval(this.intervals.connectionChecker); + + // If there is still a connection attempt outstanding then we should stop + // it before calling disconnect, otherwise it will be rejected in the onClose + // handler and trigger a retry + this.stopConnectionAttempt(); + + this.disconnect(); + + this.removeAllListeners(); + + this.cleanupWebSocket(); + } +} diff --git a/packages/provider/src/IncomingMessage.ts b/packages/provider/src/IncomingMessage.ts new file mode 100644 index 0000000..78ea676 --- /dev/null +++ b/packages/provider/src/IncomingMessage.ts @@ -0,0 +1,63 @@ +import type { Decoder } from "lib0/decoding"; +import { + createDecoder, + peekVarString, + readVarUint, + readVarUint8Array, + readVarString, +} from "lib0/decoding"; +import type { Encoder } from "lib0/encoding"; +import { + createEncoder, + writeVarUint, + writeVarUint8Array, + writeVarString, + length, +} from "lib0/encoding"; +import type { MessageType } from "./types.ts"; + +export class IncomingMessage { + data: any; + + encoder: Encoder; + + decoder: Decoder; + + constructor(data: any) { + this.data = data; + this.encoder = createEncoder(); + this.decoder = createDecoder(new Uint8Array(this.data)); + } + + peekVarString(): string { + return peekVarString(this.decoder); + } + + readVarUint(): MessageType { + return readVarUint(this.decoder); + } + + readVarString(): string { + return readVarString(this.decoder); + } + + readVarUint8Array() { + return readVarUint8Array(this.decoder); + } + + writeVarUint(type: MessageType) { + return writeVarUint(this.encoder, type); + } + + writeVarString(string: string) { + return writeVarString(this.encoder, string); + } + + writeVarUint8Array(data: Uint8Array) { + return writeVarUint8Array(this.encoder, data); + } + + length() { + return length(this.encoder); + } +} diff --git a/packages/provider/src/MessageReceiver.ts b/packages/provider/src/MessageReceiver.ts new file mode 100644 index 0000000..7942a7a --- /dev/null +++ b/packages/provider/src/MessageReceiver.ts @@ -0,0 +1,136 @@ +import { type CloseEvent, readAuthMessage } from "@hocuspocus/common"; +import { readVarInt, readVarString } from "lib0/decoding"; +import * as awarenessProtocol from "y-protocols/awareness"; +import { messageYjsSyncStep2, readSyncMessage } from "y-protocols/sync"; +import type { HocuspocusProvider } from "./HocuspocusProvider.ts"; +import type { IncomingMessage } from "./IncomingMessage.ts"; +import { OutgoingMessage } from "./OutgoingMessage.ts"; +import { MessageType } from "./types.ts"; + +export class MessageReceiver { + message: IncomingMessage; + + constructor(message: IncomingMessage) { + this.message = message; + } + + public apply(provider: HocuspocusProvider, emitSynced: boolean) { + const { message } = this; + const type = message.readVarUint(); + + const emptyMessageLength = message.length(); + + switch (type) { + case MessageType.Sync: + this.applySyncMessage(provider, emitSynced); + break; + + case MessageType.Awareness: + this.applyAwarenessMessage(provider); + break; + + case MessageType.Auth: + this.applyAuthMessage(provider); + break; + + case MessageType.QueryAwareness: + this.applyQueryAwarenessMessage(provider); + break; + + case MessageType.Stateless: + provider.receiveStateless(readVarString(message.decoder)); + break; + + case MessageType.SyncStatus: + this.applySyncStatusMessage( + provider, + readVarInt(message.decoder) === 1, + ); + break; + + case MessageType.CLOSE: { + // eslint-disable-next-line no-case-declarations + const event: CloseEvent = { + code: 1000, + reason: readVarString(message.decoder), + }; + provider.onClose(); + provider.configuration.onClose({ event }); + provider.forwardClose({ event }); + break; + } + + default: + console.error(`Can’t apply message of unknown type: ${type}`); + } + + // Reply + if (message.length() > emptyMessageLength + 1) { + // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage) + // @ts-expect-error + provider.send(OutgoingMessage, { encoder: message.encoder }); + } + } + + private applySyncMessage(provider: HocuspocusProvider, emitSynced: boolean) { + const { message } = this; + + message.writeVarUint(MessageType.Sync); + + // Apply update + const syncMessageType = readSyncMessage( + message.decoder, + message.encoder, + provider.document, + provider, + ); + + // Synced once we receive Step2 + if (emitSynced && syncMessageType === messageYjsSyncStep2) { + provider.synced = true; + } + } + + applySyncStatusMessage(provider: HocuspocusProvider, applied: boolean) { + if (applied) { + provider.decrementUnsyncedChanges(); + } + } + + private applyAwarenessMessage(provider: HocuspocusProvider) { + if (!provider.awareness) return; + + const { message } = this; + + awarenessProtocol.applyAwarenessUpdate( + provider.awareness, + message.readVarUint8Array(), + provider, + ); + } + + private applyAuthMessage(provider: HocuspocusProvider) { + const { message } = this; + + readAuthMessage( + message.decoder, + provider.sendToken.bind(provider), + provider.permissionDeniedHandler.bind(provider), + provider.authenticatedHandler.bind(provider), + ); + } + + private applyQueryAwarenessMessage(provider: HocuspocusProvider) { + if (!provider.awareness) return; + + const { message } = this; + + message.writeVarUint(MessageType.Awareness); + message.writeVarUint8Array( + awarenessProtocol.encodeAwarenessUpdate( + provider.awareness, + Array.from(provider.awareness.getStates().keys()), + ), + ); + } +} diff --git a/packages/provider/src/MessageSender.ts b/packages/provider/src/MessageSender.ts new file mode 100644 index 0000000..82b46a1 --- /dev/null +++ b/packages/provider/src/MessageSender.ts @@ -0,0 +1,22 @@ +import type { Encoder } from "lib0/encoding"; +import { toUint8Array } from "lib0/encoding"; +import type { ConstructableOutgoingMessage } from "./types.ts"; + +export class MessageSender { + encoder: Encoder; + + message: any; + + constructor(Message: ConstructableOutgoingMessage, args: any = {}) { + this.message = new Message(); + this.encoder = this.message.get(args); + } + + create() { + return toUint8Array(this.encoder); + } + + send(webSocket: any) { + webSocket?.send(this.create()); + } +} diff --git a/packages/provider/src/OutgoingMessage.ts b/packages/provider/src/OutgoingMessage.ts new file mode 100644 index 0000000..e1b5890 --- /dev/null +++ b/packages/provider/src/OutgoingMessage.ts @@ -0,0 +1,27 @@ +import type { Encoder } from "lib0/encoding"; +import { createEncoder, toUint8Array } from "lib0/encoding"; +import type { + MessageType, + OutgoingMessageArguments, + OutgoingMessageInterface, +} from "./types.ts"; + +export class OutgoingMessage implements OutgoingMessageInterface { + encoder: Encoder; + + type?: MessageType; + + description?: string; + + constructor() { + this.encoder = createEncoder(); + } + + get(args: Partial) { + return args.encoder; + } + + toUint8Array() { + return toUint8Array(this.encoder); + } +} diff --git a/packages/provider/src/OutgoingMessages/AuthenticationMessage.ts b/packages/provider/src/OutgoingMessages/AuthenticationMessage.ts new file mode 100644 index 0000000..9194430 --- /dev/null +++ b/packages/provider/src/OutgoingMessages/AuthenticationMessage.ts @@ -0,0 +1,27 @@ +import { writeVarString, writeVarUint } from "lib0/encoding"; +import { writeAuthentication } from "@hocuspocus/common"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; +import { version } from "../version.ts"; + +export class AuthenticationMessage extends OutgoingMessage { + type = MessageType.Auth; + + description = "Authentication"; + + get(args: Partial) { + if (typeof args.token === "undefined") { + throw new Error( + "The authentication message requires `token` as an argument.", + ); + } + + writeVarString(this.encoder, args.documentName!); + writeVarUint(this.encoder, this.type); + writeAuthentication(this.encoder, args.token); + writeVarString(this.encoder, version); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/AwarenessMessage.ts b/packages/provider/src/OutgoingMessages/AwarenessMessage.ts new file mode 100644 index 0000000..f2d3cef --- /dev/null +++ b/packages/provider/src/OutgoingMessages/AwarenessMessage.ts @@ -0,0 +1,41 @@ +import * as encoding from "lib0/encoding"; +import { encodeAwarenessUpdate } from "y-protocols/awareness"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class AwarenessMessage extends OutgoingMessage { + type = MessageType.Awareness; + + description = "Awareness states update"; + + get(args: Partial) { + if (typeof args.awareness === "undefined") { + throw new Error( + "The awareness message requires awareness as an argument", + ); + } + + if (typeof args.clients === "undefined") { + throw new Error("The awareness message requires clients as an argument"); + } + + encoding.writeVarString(this.encoder, args.documentName!); + encoding.writeVarUint(this.encoder, this.type); + + let awarenessUpdate; + if (args.states === undefined) { + awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients); + } else { + awarenessUpdate = encodeAwarenessUpdate( + args.awareness, + args.clients, + args.states, + ); + } + + encoding.writeVarUint8Array(this.encoder, awarenessUpdate); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/CloseMessage.ts b/packages/provider/src/OutgoingMessages/CloseMessage.ts new file mode 100644 index 0000000..e1ef465 --- /dev/null +++ b/packages/provider/src/OutgoingMessages/CloseMessage.ts @@ -0,0 +1,17 @@ +import * as encoding from "lib0/encoding"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class CloseMessage extends OutgoingMessage { + type = MessageType.CLOSE; + + description = "Ask the server to close the connection"; + + get(args: Partial) { + encoding.writeVarString(this.encoder, args.documentName!); + encoding.writeVarUint(this.encoder, this.type); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/QueryAwarenessMessage.ts b/packages/provider/src/OutgoingMessages/QueryAwarenessMessage.ts new file mode 100644 index 0000000..8ecaf8f --- /dev/null +++ b/packages/provider/src/OutgoingMessages/QueryAwarenessMessage.ts @@ -0,0 +1,17 @@ +import * as encoding from "lib0/encoding"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class QueryAwarenessMessage extends OutgoingMessage { + type = MessageType.QueryAwareness; + + description = "Queries awareness states"; + + get(args: Partial) { + encoding.writeVarString(this.encoder, args.documentName!); + encoding.writeVarUint(this.encoder, this.type); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/StatelessMessage.ts b/packages/provider/src/OutgoingMessages/StatelessMessage.ts new file mode 100644 index 0000000..6c2f7fd --- /dev/null +++ b/packages/provider/src/OutgoingMessages/StatelessMessage.ts @@ -0,0 +1,18 @@ +import { writeVarString, writeVarUint } from "lib0/encoding"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class StatelessMessage extends OutgoingMessage { + type = MessageType.Stateless; + + description = "A stateless message"; + + get(args: Partial) { + writeVarString(this.encoder, args.documentName!); + writeVarUint(this.encoder, this.type); + writeVarString(this.encoder, args.payload ?? ""); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/SyncStepOneMessage.ts b/packages/provider/src/OutgoingMessages/SyncStepOneMessage.ts new file mode 100644 index 0000000..73240f4 --- /dev/null +++ b/packages/provider/src/OutgoingMessages/SyncStepOneMessage.ts @@ -0,0 +1,25 @@ +import * as encoding from "lib0/encoding"; +import * as syncProtocol from "y-protocols/sync"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class SyncStepOneMessage extends OutgoingMessage { + type = MessageType.Sync; + + description = "First sync step"; + + get(args: Partial) { + if (typeof args.document === "undefined") { + throw new Error( + "The sync step one message requires document as an argument", + ); + } + + encoding.writeVarString(this.encoder, args.documentName!); + encoding.writeVarUint(this.encoder, this.type); + syncProtocol.writeSyncStep1(this.encoder, args.document); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/SyncStepTwoMessage.ts b/packages/provider/src/OutgoingMessages/SyncStepTwoMessage.ts new file mode 100644 index 0000000..02d96ab --- /dev/null +++ b/packages/provider/src/OutgoingMessages/SyncStepTwoMessage.ts @@ -0,0 +1,25 @@ +import * as encoding from "lib0/encoding"; +import * as syncProtocol from "y-protocols/sync"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class SyncStepTwoMessage extends OutgoingMessage { + type = MessageType.Sync; + + description = "Second sync step"; + + get(args: Partial) { + if (typeof args.document === "undefined") { + throw new Error( + "The sync step two message requires document as an argument", + ); + } + + encoding.writeVarString(this.encoder, args.documentName!); + encoding.writeVarUint(this.encoder, this.type); + syncProtocol.writeSyncStep2(this.encoder, args.document); + + return this.encoder; + } +} diff --git a/packages/provider/src/OutgoingMessages/UpdateMessage.ts b/packages/provider/src/OutgoingMessages/UpdateMessage.ts new file mode 100644 index 0000000..7f9ed9a --- /dev/null +++ b/packages/provider/src/OutgoingMessages/UpdateMessage.ts @@ -0,0 +1,20 @@ +import { writeVarString, writeVarUint } from "lib0/encoding"; +import { writeUpdate } from "y-protocols/sync"; +import type { OutgoingMessageArguments } from "../types.ts"; +import { MessageType } from "../types.ts"; +import { OutgoingMessage } from "../OutgoingMessage.ts"; + +export class UpdateMessage extends OutgoingMessage { + type = MessageType.Sync; + + description = "A document update"; + + get(args: Partial) { + writeVarString(this.encoder, args.documentName!); + writeVarUint(this.encoder, this.type); + + writeUpdate(this.encoder, args.update); + + return this.encoder; + } +} diff --git a/packages/provider/src/index.ts b/packages/provider/src/index.ts new file mode 100644 index 0000000..c54d369 --- /dev/null +++ b/packages/provider/src/index.ts @@ -0,0 +1,3 @@ +export * from "./HocuspocusProvider.ts"; +export * from "./HocuspocusProviderWebsocket.ts"; +export * from "./types.ts"; diff --git a/packages/provider/src/types.ts b/packages/provider/src/types.ts new file mode 100644 index 0000000..be3a219 --- /dev/null +++ b/packages/provider/src/types.ts @@ -0,0 +1,121 @@ +import type { Encoder } from "lib0/encoding"; +import type { Awareness } from "y-protocols/awareness"; +import type * as Y from "yjs"; +import type { CloseEvent } from "@hocuspocus/common"; +import type { IncomingMessage } from "./IncomingMessage.ts"; +import type { OutgoingMessage } from "./OutgoingMessage.ts"; +import type { AuthenticationMessage } from "./OutgoingMessages/AuthenticationMessage.ts"; +import type { AwarenessMessage } from "./OutgoingMessages/AwarenessMessage.ts"; +import type { QueryAwarenessMessage } from "./OutgoingMessages/QueryAwarenessMessage.ts"; +import type { SyncStepOneMessage } from "./OutgoingMessages/SyncStepOneMessage.ts"; +import type { SyncStepTwoMessage } from "./OutgoingMessages/SyncStepTwoMessage.ts"; +import type { UpdateMessage } from "./OutgoingMessages/UpdateMessage.ts"; + +export enum MessageType { + Sync = 0, + Awareness = 1, + Auth = 2, + QueryAwareness = 3, + Stateless = 5, + CLOSE = 7, + SyncStatus = 8, + Ping = 9, + Pong = 10, +} + +export enum WebSocketStatus { + Connecting = "connecting", + Connected = "connected", + Disconnected = "disconnected", +} + +export type AuthorizedScope = "read-write" | "readonly"; + +export interface OutgoingMessageInterface { + encoder: Encoder; + type?: MessageType; + description?: string; +} + +export interface OutgoingMessageArguments { + documentName: string; + token: string; + document: Y.Doc; + awareness: Awareness; + clients: number[]; + states: Map; + update: any; + payload: string; + encoder: Encoder; +} + +export interface Constructable { + new (...args: any): T; +} + +export type ConstructableOutgoingMessage = + | Constructable + | Constructable + | Constructable + | Constructable + | Constructable + | Constructable; + +export type onAuthenticationFailedParameters = { + reason: string; +}; + +export type onAuthenticatedParameters = { + scope: AuthorizedScope; +}; + +export type onOpenParameters = { + event: Event; +}; + +export type onMessageParameters = { + event: MessageEvent; + message: IncomingMessage; +}; + +export type onOutgoingMessageParameters = { + message: OutgoingMessage; +}; + +export type onStatusParameters = { + status: WebSocketStatus; +}; + +export type onSyncedParameters = { + state: boolean; +}; + +export type onUnsyncedChangesParameters = { + number: number; +}; + +export type onDisconnectParameters = { + event: CloseEvent; +}; + +export type onCloseParameters = { + event: CloseEvent; +}; + +export type onMaxAttemptsFailedParameters = { + error: unknown; +}; + +export type onAwarenessUpdateParameters = { + states: StatesArray; +}; + +export type onAwarenessChangeParameters = { + states: StatesArray; +}; + +export type onStatelessParameters = { + payload: string; +}; + +export type StatesArray = { clientId: number; [key: string | number]: any }[]; diff --git a/packages/provider/src/version.ts b/packages/provider/src/version.ts new file mode 100644 index 0000000..9387e2a --- /dev/null +++ b/packages/provider/src/version.ts @@ -0,0 +1,6 @@ +export const version: string = + // @ts-expect-error - __HOCUSPOCUS_VERSION__ is replaced at build time by rolldown + typeof __HOCUSPOCUS_VERSION__ !== "undefined" + ? // @ts-expect-error - __HOCUSPOCUS_VERSION__ is replaced at build time by rolldown + __HOCUSPOCUS_VERSION__ + : "unknown"; diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md new file mode 100644 index 0000000..47f0ae1 --- /dev/null +++ b/packages/server/CHANGELOG.md @@ -0,0 +1,2176 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + + +### Bug Fixes + +* encode stateless message once when received operation via Redis ; this is a performance fix. ([#1136](https://github.com/ueberdosis/hocuspocus/issues/1136)) ([b524b4b](https://github.com/ueberdosis/hocuspocus/commit/b524b4b30299a64ffa1309f70a0fd6e761103d4a)) + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) +* when beforeHandleMessage throws, we don't want to process other messages that were already queued ([#1123](https://github.com/ueberdosis/hocuspocus/issues/1123)) ([ed5dc40](https://github.com/ueberdosis/hocuspocus/commit/ed5dc40581cc829a6d0b04040717a8ee89296140)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + + +### Bug Fixes + +* allow binding the server to a specific address ([#1121](https://github.com/ueberdosis/hocuspocus/issues/1121)) ([408127b](https://github.com/ueberdosis/hocuspocus/commit/408127b1c090356cc9148a801f314a8e6f863b09)) + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + + +### Features + +* add `afterHandleMessage` hook to run after message handling completion ([#1112](https://github.com/ueberdosis/hocuspocus/issues/1112)) ([abec66d](https://github.com/ueberdosis/hocuspocus/commit/abec66db76ab43737e3e5baf8a461ea38637d5d6)) +* enforce pre-auth resource limits to safeguard server stability ([#1113](https://github.com/ueberdosis/hocuspocus/issues/1113)) ([6e1a932](https://github.com/ueberdosis/hocuspocus/commit/6e1a932662f38074b732b5e51394461b91809570)) + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + + +### Features + +* add `unloadImmediately` option to `disconnect()` for configurable document persistence behavior ([#1111](https://github.com/ueberdosis/hocuspocus/issues/1111)) ([eeb4696](https://github.com/ueberdosis/hocuspocus/commit/eeb469630540b8aeb08d699ba130a86e50e7ee5e)) + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + + +### Bug Fixes + +* delete debouncer timer entry before awaiting the in-flight execution ([#1110](https://github.com/ueberdosis/hocuspocus/issues/1110)) ([f05f1d0](https://github.com/ueberdosis/hocuspocus/commit/f05f1d02f7890822e76d7b16f3cf3b3d9901ab02)), closes [#1014](https://github.com/ueberdosis/hocuspocus/issues/1014) + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + + +### Bug Fixes + +* client-initiated broadcastStateless ([#1103](https://github.com/ueberdosis/hocuspocus/issues/1103)) ([6872d08](https://github.com/ueberdosis/hocuspocus/commit/6872d08bbb2ebe8175dcfb51953481b2f4988291)) + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + + +### Bug Fixes + +* **server:** wrap awareness origin as TransactionOrigin ([#1094](https://github.com/ueberdosis/hocuspocus/issues/1094)) ([bb24de0](https://github.com/ueberdosis/hocuspocus/commit/bb24de0ffb944e10845174e26971eaa95b2db675)) + + +### Features + +* **server:** add beforeHandleAwareness hook ([#1096](https://github.com/ueberdosis/hocuspocus/issues/1096)) ([1597da8](https://github.com/ueberdosis/hocuspocus/commit/1597da8217c4ed20b06d46052f1db7c7765d6153)) + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Bug Fixes + +* remove unused packages ([#1090](https://github.com/ueberdosis/hocuspocus/issues/1090)) ([66fb35c](https://github.com/ueberdosis/hocuspocus/commit/66fb35cff71d986d671fe9a8118ce4da87507b44)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + + +### Bug Fixes + +* when storeDocumentHooks throw, retry until storing suceeds. ([99db64d](https://github.com/ueberdosis/hocuspocus/commit/99db64df234946f2492129e793298734c5e2c503)) + + +### Features + +* optimize retry logic ([302eb08](https://github.com/ueberdosis/hocuspocus/commit/302eb08d9a2a2b59d44424571fdc70d5790d8fd8)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + + +### Bug Fixes + +* check code is type number ([#1062](https://github.com/ueberdosis/hocuspocus/issues/1062)) ([6dfa610](https://github.com/ueberdosis/hocuspocus/commit/6dfa610b35fa1aab7a7f0dd2f2a216cc3094612f)) +* force-run debounced ops on server shutdown (even when unloadImmediately=false) ([dba9984](https://github.com/ueberdosis/hocuspocus/commit/dba99845a52e798884cda7e2c81d16e4dc462e9c)) +* onLoadDocument now accepts a yjs update (Uint8Array or Buffer) or a Y.Doc. fixes [#795](https://github.com/ueberdosis/hocuspocus/issues/795), [#271](https://github.com/ueberdosis/hocuspocus/issues/271) ([667e145](https://github.com/ueberdosis/hocuspocus/commit/667e14535d2da3986ecc731b18c976b3662552aa)) +* optimize memory usage by creating message uint8arrays once, not per connection ([#1051](https://github.com/ueberdosis/hocuspocus/issues/1051)) ([ad398a8](https://github.com/ueberdosis/hocuspocus/commit/ad398a8304908068df8e11c87edf9962f416751a)) +* reset auth state on failed auth, so a new auth message can run auth hooks again. fixes [#944](https://github.com/ueberdosis/hocuspocus/issues/944) ([#1065](https://github.com/ueberdosis/hocuspocus/issues/1065)) ([81ce838](https://github.com/ueberdosis/hocuspocus/commit/81ce8382a80509bad7d8c6581ff87e9d91b26179)) +* trigger storeDocumentHooks on any change to the doc (except if incoming via redis). Before, it was easy to accidentally prevent changes from being saved by forgetting to include an origin in the Yjs update ([81975e7](https://github.com/ueberdosis/hocuspocus/commit/81975e7a38b4b594024863278e61ce5ecf318121)) +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + +### Features + +* add generic `Context` support across server classes and hooks ([#1057](https://github.com/ueberdosis/hocuspocus/issues/1057)) ([67741a0](https://github.com/ueberdosis/hocuspocus/commit/67741a0661004f1d222795bb99e68d27a5a701aa)) +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* pass context when using DirectConnection. Do not run storeDocumentHooks on any transact, just on disconnect ([ffa32b7](https://github.com/ueberdosis/hocuspocus/commit/ffa32b7a96a15d953b031df1005df6cfe7624d51)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + + +### Bug Fixes + +* force-run debounced ops on server shutdown (even when unloadImmediately=false) ([dba9984](https://github.com/ueberdosis/hocuspocus/commit/dba99845a52e798884cda7e2c81d16e4dc462e9c)) +* upgrade deps ([f3fc252](https://github.com/ueberdosis/hocuspocus/commit/f3fc252c3978df3229813f370d80ff3f2f0759d0)) + + +### Features + +* make the server aware of the provider version, so future protocol changes are easier to make backward-compatible. ([2b47dc2](https://github.com/ueberdosis/hocuspocus/commit/2b47dc2ede1bf15ff90f1048352f103cea57f300)) +* session awareness ([#1071](https://github.com/ueberdosis/hocuspocus/issues/1071)) ([00dd3eb](https://github.com/ueberdosis/hocuspocus/commit/00dd3ebf856c79443f7bf7e6ea8e7f42015bb3d4)) + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + + +### Bug Fixes + +* check code is type number ([#1062](https://github.com/ueberdosis/hocuspocus/issues/1062)) ([6dfa610](https://github.com/ueberdosis/hocuspocus/commit/6dfa610b35fa1aab7a7f0dd2f2a216cc3094612f)) +* onLoadDocument now accepts a yjs update (Uint8Array or Buffer) or a Y.Doc. fixes [#795](https://github.com/ueberdosis/hocuspocus/issues/795), [#271](https://github.com/ueberdosis/hocuspocus/issues/271) ([667e145](https://github.com/ueberdosis/hocuspocus/commit/667e14535d2da3986ecc731b18c976b3662552aa)) +* optimize memory usage by creating message uint8arrays once, not per connection ([#1051](https://github.com/ueberdosis/hocuspocus/issues/1051)) ([ad398a8](https://github.com/ueberdosis/hocuspocus/commit/ad398a8304908068df8e11c87edf9962f416751a)) +* reset auth state on failed auth, so a new auth message can run auth hooks again. fixes [#944](https://github.com/ueberdosis/hocuspocus/issues/944) ([#1065](https://github.com/ueberdosis/hocuspocus/issues/1065)) ([81ce838](https://github.com/ueberdosis/hocuspocus/commit/81ce8382a80509bad7d8c6581ff87e9d91b26179)) +* trigger storeDocumentHooks on any change to the doc (except if incoming via redis). Before, it was easy to accidentally prevent changes from being saved by forgetting to include an origin in the Yjs update ([81975e7](https://github.com/ueberdosis/hocuspocus/commit/81975e7a38b4b594024863278e61ce5ecf318121)) + + +### Features + +* add generic `Context` support across server classes and hooks ([#1057](https://github.com/ueberdosis/hocuspocus/issues/1057)) ([67741a0](https://github.com/ueberdosis/hocuspocus/commit/67741a0661004f1d222795bb99e68d27a5a701aa)) +* better type safety for yjs origin. Improve type safety and allow any yjs operation to skip store document hooks. ([5f480cd](https://github.com/ueberdosis/hocuspocus/commit/5f480cd07de378704a8b13780460e43ab7917655)) +* cross-ws (bun, deno, cloudflare, node uwebsocket) ([#1056](https://github.com/ueberdosis/hocuspocus/issues/1056)) ([2666542](https://github.com/ueberdosis/hocuspocus/commit/2666542aafafb6b055c6ac7b22ba50a6f28d4e9a)) +* migrate to pnpm ([#1061](https://github.com/ueberdosis/hocuspocus/issues/1061)) ([dcaab57](https://github.com/ueberdosis/hocuspocus/commit/dcaab571bad694e48efd41bbb1d675855a7e2055)) +* pass context when using DirectConnection. Do not run storeDocumentHooks on any transact, just on disconnect ([ffa32b7](https://github.com/ueberdosis/hocuspocus/commit/ffa32b7a96a15d953b031df1005df6cfe7624d51)) +* process update message in the order that they are received ([#1058](https://github.com/ueberdosis/hocuspocus/issues/1058)) ([b6162ac](https://github.com/ueberdosis/hocuspocus/commit/b6162ac9154d0e56bca748917e4ebe1e3fb071cf)) + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + + +### Bug Fixes + +* fixes typings for onTokenSync ([b2ac132](https://github.com/ueberdosis/hocuspocus/commit/b2ac13250b2ddc6bb41ce456b56bac8db2a6e0d5)) + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + + +### Bug Fixes + +* fixes memory leak under high load, replace nextTick by setTimeout ([1f0b5a4](https://github.com/ueberdosis/hocuspocus/commit/1f0b5a4f9084c0b6e01ddd19ec831d2e2664fc23)) + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + + +### Bug Fixes + +* fixes timing issues with debouncer: new debounced functions may start before the previous had finished ([708bfdb](https://github.com/ueberdosis/hocuspocus/commit/708bfdb0197833d5d6371b4f2c930fe815a66d15)) +* make beforeSync not async, as this may break message processing order (see https://github.com/yjs/yjs/issues/591) ([abeb298](https://github.com/ueberdosis/hocuspocus/commit/abeb2985d522539d30ea96217417a300569f77fd)) +* make handleUpdate sync to avoid timing issues ([9a6485b](https://github.com/ueberdosis/hocuspocus/commit/9a6485b9c0ee2f193e9ba68b39a8c93196a796bf)) + + +### Features + +* adds Document.lastChangeTime, which is set to Date.now() whenever a change on the documment has been detected ([9092efd](https://github.com/ueberdosis/hocuspocus/commit/9092efd604f20e1ec5228a93dd8b1ee9b48d5e98)) + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + + +### Reverts + +* Revert "fix: temporarily removes beforeSync callback (#919, partially reverts a6a7bcd0768378908ffb5d32096183280115631b). Making applySync async causes timing issues that leads to issues when using PermanentUserData. This can be reproduced by using the playground server with sqlite and a sleep(1000). Additionally, use Y.PermanentUserData, or try to read data from the ydoc in the onSynced event." ([e33dbe2](https://github.com/ueberdosis/hocuspocus/commit/e33dbe22d6ec98279699e11ef6d5bb224299e2d3)), closes [#919](https://github.com/ueberdosis/hocuspocus/issues/919) + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + + +### Bug Fixes + +* temporarily removes beforeSync callback ([#919](https://github.com/ueberdosis/hocuspocus/issues/919), partially reverts a6a7bcd0768378908ffb5d32096183280115631b). Making applySync async causes timing issues that leads to issues when using PermanentUserData. This can be reproduced by using the playground server with sqlite and a sleep(1000). Additionally, use Y.PermanentUserData, or try to read data from the ydoc in the onSynced event. ([7192664](https://github.com/ueberdosis/hocuspocus/commit/719266443e13e0dab3f69cd8e10b29da547ceab1)) + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + + +### Bug Fixes + +* fixes timing issues when multiple users leave the same document together ([70e5955](https://github.com/ueberdosis/hocuspocus/commit/70e59553e95acbeaf170e0e5a13ea14cfd4ff152)) + + +### Features + +* add onTokenSync hook for auth token re-sync ([#1001](https://github.com/ueberdosis/hocuspocus/issues/1001)) ([ee252fb](https://github.com/ueberdosis/hocuspocus/commit/ee252fb99ea6faf80cc71d2e1ce51726e698b76b)) + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + + +### Bug Fixes + +* bind onUpdate before afterLoadDocument, as changes wont be saved otherwise ([bf8a6f9](https://github.com/ueberdosis/hocuspocus/commit/bf8a6f9b2902a81664bbbba8e8a34f7112570d2a)) + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Bug Fixes + +* delay adding document to instance until connection is about to be established ([#972](https://github.com/ueberdosis/hocuspocus/issues/972)) ([1e2917a](https://github.com/ueberdosis/hocuspocus/commit/1e2917aacb342c035645bb3c01af4910d699eaad)) +* set max listeners to infinity to avoid warnings when using multiplexing ([21412aa](https://github.com/ueberdosis/hocuspocus/commit/21412aa3f4bc861fa4ece46c2e57d5a0a48be8af)) + + +### Features + +* remove uuid dependency and replace usages by crypto.randomUUID ([#1005](https://github.com/ueberdosis/hocuspocus/issues/1005)) ([393189f](https://github.com/ueberdosis/hocuspocus/commit/393189fe448c68cbd741642866be4b4b23546f17)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + + +### Bug Fixes + +* unload document in catch block of storeDocumentHooks ([#955](https://github.com/ueberdosis/hocuspocus/issues/955)) ([1a3b5b8](https://github.com/ueberdosis/hocuspocus/commit/1a3b5b8b2057e11ea16068f17efadd5c93437276)) + + +### Features + +* adds more error logging when a connection is closed due to an exception ([1c4090c](https://github.com/ueberdosis/hocuspocus/commit/1c4090cc464d724e74143296b6e05bde87f5f5bf)) + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + + +### Bug Fixes + +* add catch on message receiver when handling message ([#967](https://github.com/ueberdosis/hocuspocus/issues/967)) ([f9ceb26](https://github.com/ueberdosis/hocuspocus/commit/f9ceb2626674d173037be8f8bf25179719ddf977)) + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + + +### Features + +* adds `document` to `beforeUnloadDocumentPayload` ([f7d1e64](https://github.com/ueberdosis/hocuspocus/commit/f7d1e646342746b9228fe1a8df8a80ee35e2aea5)) +* beforeUnloadDocument: allow passing in Hocuspocus constructor ; allow preventing unload by throwing an error in beforeUnloadDocument ([63b298e](https://github.com/ueberdosis/hocuspocus/commit/63b298eddf23b8f472a066dc8274b22d790176ea)) + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + + +### Features + +* add beforeSync hook ([#919](https://github.com/ueberdosis/hocuspocus/issues/919)) ([a6a7bcd](https://github.com/ueberdosis/hocuspocus/commit/a6a7bcd0768378908ffb5d32096183280115631b)) + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + + +### Bug Fixes + +* Memory leak in Direct Connection ([#895](https://github.com/ueberdosis/hocuspocus/issues/895)) ([70ab0e2](https://github.com/ueberdosis/hocuspocus/commit/70ab0e20e645e2ddd358847569b7e670622bbcc9)), closes [#2](https://github.com/ueberdosis/hocuspocus/issues/2) +* **server:** fast ws disconnect handling during conn setup ([#900](https://github.com/ueberdosis/hocuspocus/issues/900)) ([6817b53](https://github.com/ueberdosis/hocuspocus/commit/6817b535f63309c5ffbe59e3818c1ceb4749d0a7)) + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + + +### Bug Fixes + +* pass `WebSocket` instead of Connection to `applyAwarenessUpdate` ([#882](https://github.com/ueberdosis/hocuspocus/issues/882)) ([e359652](https://github.com/ueberdosis/hocuspocus/commit/e3596525072261a04f7c9e62888c06ea33f886dd)) + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + + +### Bug Fixes + +* check if ymap is empty ([#666](https://github.com/ueberdosis/hocuspocus/issues/666)) ([4450372](https://github.com/ueberdosis/hocuspocus/commit/4450372e7bff40693d32aa6b6c425dcfdb868ded)) + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + + +### Bug Fixes + +* connection.sendStateless error when connections more than one ([#556](https://github.com/ueberdosis/hocuspocus/issues/556)) ([9a9260f](https://github.com/ueberdosis/hocuspocus/commit/9a9260ffd5ee559bdc4f8ba4e7c1b1c12c2944d0)) + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Bug Fixes + +* Empty sync message causes error in client MessageReceiver ([#174](https://github.com/ueberdosis/hocuspocus/issues/174)) ([f9dca69](https://github.com/ueberdosis/hocuspocus/commit/f9dca69eb96d1ede37a0709bd3b7735bf1ff57ba)) +* fix hook promise chaining ([ee5052d](https://github.com/ueberdosis/hocuspocus/commit/ee5052d236ba0b400880dc7ca1c90cefdd372003)) +* Potential onCreateDocument race condition ([#167](https://github.com/ueberdosis/hocuspocus/issues/167)) ([b3e3e4d](https://github.com/ueberdosis/hocuspocus/commit/b3e3e4dea74f9b833ccb0c6a6521f55c001411c1)) +* Remove event listener once unused ([#220](https://github.com/ueberdosis/hocuspocus/issues/220)) ([0422196](https://github.com/ueberdosis/hocuspocus/commit/0422196f8a4e09af530c51419742b137b9ebbc69)) +* typescript strings ([0dd5f12](https://github.com/ueberdosis/hocuspocus/commit/0dd5f1292616e426cdb4cc79e83ab8ced0895bfa)) + + +### Features + +* Add connectionsCount and documentsCount methods to server ([8bdbcd8](https://github.com/ueberdosis/hocuspocus/commit/8bdbcd86b1f18462f6636b75a4cbd97ebefdb227)) +* add read only mode ([7b59d52](https://github.com/ueberdosis/hocuspocus/commit/7b59d522b966b51347db35ac6a4524211e44ae9c)) +* add request headers and parameters to onCreateDocument ([47a8b95](https://github.com/ueberdosis/hocuspocus/commit/47a8b95baf8dd22ebd71c56565420179402cdaa4)) +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) + + + + + +# [1.0.0-alpha.107](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.106...@hocuspocus/server@1.0.0-alpha.107) (2022-09-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.106](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.105...@hocuspocus/server@1.0.0-alpha.106) (2022-09-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.105](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.104...@hocuspocus/server@1.0.0-alpha.105) (2022-08-28) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.104](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.103...@hocuspocus/server@1.0.0-alpha.104) (2022-07-13) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.103](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.102...@hocuspocus/server@1.0.0-alpha.103) (2022-06-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.102](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.101...@hocuspocus/server@1.0.0-alpha.102) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.101](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.100...@hocuspocus/server@1.0.0-alpha.101) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.100](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.99...@hocuspocus/server@1.0.0-alpha.100) (2022-03-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.99](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.98...@hocuspocus/server@1.0.0-alpha.99) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.98](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.97...@hocuspocus/server@1.0.0-alpha.98) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.97](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.96...@hocuspocus/server@1.0.0-alpha.97) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.96](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.95...@hocuspocus/server@1.0.0-alpha.96) (2022-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.95](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.94...@hocuspocus/server@1.0.0-alpha.95) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.94](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.93...@hocuspocus/server@1.0.0-alpha.94) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.93](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.92...@hocuspocus/server@1.0.0-alpha.93) (2022-02-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.92](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.91...@hocuspocus/server@1.0.0-alpha.92) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.91](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.90...@hocuspocus/server@1.0.0-alpha.91) (2022-01-12) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.90](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.89...@hocuspocus/server@1.0.0-alpha.90) (2021-12-13) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.89](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.88...@hocuspocus/server@1.0.0-alpha.89) (2021-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.88](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.87...@hocuspocus/server@1.0.0-alpha.88) (2021-12-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.87](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.86...@hocuspocus/server@1.0.0-alpha.87) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.86](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.85...@hocuspocus/server@1.0.0-alpha.86) (2021-12-03) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.85](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.84...@hocuspocus/server@1.0.0-alpha.85) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.84](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.83...@hocuspocus/server@1.0.0-alpha.84) (2021-12-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.83](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.82...@hocuspocus/server@1.0.0-alpha.83) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.82](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.81...@hocuspocus/server@1.0.0-alpha.82) (2021-11-30) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.81](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.80...@hocuspocus/server@1.0.0-alpha.81) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.80](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.79...@hocuspocus/server@1.0.0-alpha.80) (2021-11-24) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.79](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.78...@hocuspocus/server@1.0.0-alpha.79) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.78](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.77...@hocuspocus/server@1.0.0-alpha.78) (2021-11-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.77](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.76...@hocuspocus/server@1.0.0-alpha.77) (2021-11-05) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.76](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.75...@hocuspocus/server@1.0.0-alpha.76) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.75](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.74...@hocuspocus/server@1.0.0-alpha.75) (2021-10-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.74](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.73...@hocuspocus/server@1.0.0-alpha.74) (2021-10-08) + + +### Bug Fixes + +* Remove event listener once unused ([#220](https://github.com/ueberdosis/hocuspocus/issues/220)) ([0422196](https://github.com/ueberdosis/hocuspocus/commit/0422196f8a4e09af530c51419742b137b9ebbc69)) + + + + + +# [1.0.0-alpha.73](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.71...@hocuspocus/server@1.0.0-alpha.73) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.71](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.70...@hocuspocus/server@1.0.0-alpha.71) (2021-09-23) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.70](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.69...@hocuspocus/server@1.0.0-alpha.70) (2021-09-17) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.69](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.68...@hocuspocus/server@1.0.0-alpha.69) (2021-09-14) + + +### Features + +* Add connectionsCount and documentsCount methods to server ([8bdbcd8](https://github.com/ueberdosis/hocuspocus/commit/8bdbcd86b1f18462f6636b75a4cbd97ebefdb227)) + + + + + +# [1.0.0-alpha.68](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.67...@hocuspocus/server@1.0.0-alpha.68) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.67](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.66...@hocuspocus/server@1.0.0-alpha.67) (2021-09-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.66](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.65...@hocuspocus/server@1.0.0-alpha.66) (2021-08-31) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.65](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.64...@hocuspocus/server@1.0.0-alpha.65) (2021-08-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.64](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.63...@hocuspocus/server@1.0.0-alpha.64) (2021-08-27) + + +### Bug Fixes + +* Empty sync message causes error in client MessageReceiver ([#174](https://github.com/ueberdosis/hocuspocus/issues/174)) ([f9dca69](https://github.com/ueberdosis/hocuspocus/commit/f9dca69eb96d1ede37a0709bd3b7735bf1ff57ba)) +* Potential onCreateDocument race condition ([#167](https://github.com/ueberdosis/hocuspocus/issues/167)) ([b3e3e4d](https://github.com/ueberdosis/hocuspocus/commit/b3e3e4dea74f9b833ccb0c6a6521f55c001411c1)) + + + + + +# [1.0.0-alpha.63](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.62...@hocuspocus/server@1.0.0-alpha.63) (2021-08-19) + + +### Features + +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) + + + + + +# [1.0.0-alpha.62](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.61...@hocuspocus/server@1.0.0-alpha.62) (2021-08-19) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.61](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.60...@hocuspocus/server@1.0.0-alpha.61) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.60](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.59...@hocuspocus/server@1.0.0-alpha.60) (2021-07-13) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.59](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.58...@hocuspocus/server@1.0.0-alpha.59) (2021-06-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.58](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.57...@hocuspocus/server@1.0.0-alpha.58) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.57](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.56...@hocuspocus/server@1.0.0-alpha.57) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.56](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.55...@hocuspocus/server@1.0.0-alpha.56) (2021-06-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.55](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.54...@hocuspocus/server@1.0.0-alpha.55) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.54](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.53...@hocuspocus/server@1.0.0-alpha.54) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.53](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.52...@hocuspocus/server@1.0.0-alpha.53) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.52](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.51...@hocuspocus/server@1.0.0-alpha.52) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.51](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.50...@hocuspocus/server@1.0.0-alpha.51) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.50](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.49...@hocuspocus/server@1.0.0-alpha.50) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.49](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.48...@hocuspocus/server@1.0.0-alpha.49) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.48](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.47...@hocuspocus/server@1.0.0-alpha.48) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.47](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.46...@hocuspocus/server@1.0.0-alpha.47) (2021-04-15) + + +### Features + +* add request headers and parameters to onCreateDocument ([47a8b95](https://github.com/ueberdosis/hocuspocus/commit/47a8b95baf8dd22ebd71c56565420179402cdaa4)) + + + + + +# [1.0.0-alpha.46](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.45...@hocuspocus/server@1.0.0-alpha.46) (2021-04-14) + + +### Features + +* add read only mode ([7b59d52](https://github.com/ueberdosis/hocuspocus/commit/7b59d522b966b51347db35ac6a4524211e44ae9c)) + + + + + +# [1.0.0-alpha.45](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.44...@hocuspocus/server@1.0.0-alpha.45) (2021-04-12) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.44](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.43...@hocuspocus/server@1.0.0-alpha.44) (2021-04-08) + + +### Bug Fixes + +* typescript strings ([0dd5f12](https://github.com/ueberdosis/hocuspocus/commit/0dd5f1292616e426cdb4cc79e83ab8ced0895bfa)) + + + + + +# [1.0.0-alpha.43](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.42...@hocuspocus/server@1.0.0-alpha.43) (2021-04-07) + + +### Bug Fixes + +* fix hook promise chaining ([ee5052d](https://github.com/ueberdosis/hocuspocus/commit/ee5052d236ba0b400880dc7ca1c90cefdd372003)) + + + + + +# [1.0.0-alpha.42](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.41...@hocuspocus/server@1.0.0-alpha.42) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.41](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.40...@hocuspocus/server@1.0.0-alpha.41) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.40](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.39...@hocuspocus/server@1.0.0-alpha.40) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.39](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.38...@hocuspocus/server@1.0.0-alpha.39) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.38](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.37...@hocuspocus/server@1.0.0-alpha.38) (2021-03-25) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.36...@hocuspocus/server@1.0.0-alpha.37) (2021-03-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.35...@hocuspocus/server@1.0.0-alpha.36) (2021-03-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.34...@hocuspocus/server@1.0.0-alpha.35) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.34](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.33...@hocuspocus/server@1.0.0-alpha.34) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.33](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.32...@hocuspocus/server@1.0.0-alpha.33) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.32](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.31...@hocuspocus/server@1.0.0-alpha.32) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.31](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.30...@hocuspocus/server@1.0.0-alpha.31) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.30](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.29...@hocuspocus/server@1.0.0-alpha.30) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.29](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.28...@hocuspocus/server@1.0.0-alpha.29) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.28](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.27...@hocuspocus/server@1.0.0-alpha.28) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.27](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.26...@hocuspocus/server@1.0.0-alpha.27) (2021-03-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.26](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.25...@hocuspocus/server@1.0.0-alpha.26) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.25](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.24...@hocuspocus/server@1.0.0-alpha.25) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.24](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.23...@hocuspocus/server@1.0.0-alpha.24) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.22...@hocuspocus/server@1.0.0-alpha.23) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.21...@hocuspocus/server@1.0.0-alpha.22) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.20...@hocuspocus/server@1.0.0-alpha.21) (2021-03-04) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.19...@hocuspocus/server@1.0.0-alpha.20) (2021-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.18...@hocuspocus/server@1.0.0-alpha.19) (2021-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.17...@hocuspocus/server@1.0.0-alpha.18) (2021-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.16...@hocuspocus/server@1.0.0-alpha.17) (2021-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.15...@hocuspocus/server@1.0.0-alpha.16) (2021-03-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.14...@hocuspocus/server@1.0.0-alpha.15) (2021-02-22) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.13...@hocuspocus/server@1.0.0-alpha.14) (2021-02-18) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.12...@hocuspocus/server@1.0.0-alpha.13) (2021-02-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.11...@hocuspocus/server@1.0.0-alpha.12) (2021-02-15) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.10...@hocuspocus/server@1.0.0-alpha.11) (2021-02-01) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.9...@hocuspocus/server@1.0.0-alpha.10) (2021-01-26) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.8...@hocuspocus/server@1.0.0-alpha.9) (2021-01-21) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.7...@hocuspocus/server@1.0.0-alpha.8) (2021-01-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.6...@hocuspocus/server@1.0.0-alpha.7) (2021-01-11) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.5...@hocuspocus/server@1.0.0-alpha.6) (2020-12-10) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.3...@hocuspocus/server@1.0.0-alpha.5) (2020-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server@1.0.0-alpha.1...@hocuspocus/server@1.0.0-alpha.3) (2020-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.1 (2020-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.1 (2020-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.1 (2020-12-09) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.1 (2020-12-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.2 (2020-12-02) + +**Note:** Version bump only for package @hocuspocus/server + + + + + +# 1.0.0-alpha.1 (2020-12-02) + +**Note:** Version bump only for package @hocuspocus/server diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..b985a8e --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1,107 @@ +# @hocuspocus/server +[![Version](https://img.shields.io/npm/v/@hocuspocus/server.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/server) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/server.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/server.svg)](https://www.npmjs.com/package/@hocuspocus/server) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +The collaborative editing backend for [Tiptap](https://github.com/ueberdosis/tiptap). Built on [Y.js](https://github.com/yjs/yjs), runs on Node.js (22+), Bun, Deno, and Cloudflare Workers. + +> [!TIP] +> Help us **chart the future of Hocuspocus** by telling us about your needs in a short survey. Plus, you’ll get a chance to earn a $50 gift card. +> **[Take the survey →](https://docs.google.com/forms/d/e/1FAIpQLSeUS8S1iZTud58GBSD_d4c8lfUvkZHvzU99kWxP1wnNZnPKpw/viewform?usp=header)** + +## Installation + +```bash +npm install @hocuspocus/server +``` + +## Usage + +Minimal WebSocket server on port `1234`: + +```js +import { Server } from "@hocuspocus/server" + +const server = new Server({ + port: 1234, +}) + +server.listen() +``` + +Hook into the document lifecycle: + +```js +import { Server } from "@hocuspocus/server" + +const server = new Server({ + port: 1234, + + async onAuthenticate({ token }) { + if (token !== "super-secret-token") { + throw new Error("Not authorized!") + } + }, + + async onLoadDocument({ documentName }) { + // return a Y.Doc for new documents, or nothing to use the default empty doc + }, + + async onStoreDocument({ documentName, document }) { + // persist the Y.Doc binary state wherever you like + }, +}) + +server.listen() +``` + +For a database-backed server, combine with an extension like [`@hocuspocus/extension-sqlite`](../extension-sqlite) or [`@hocuspocus/extension-database`](../extension-database). + +## Resource limits + +Messages received from a client before authentication completes are buffered in +memory. To prevent an unauthenticated client from exhausting server memory, the +server bounds this buffer per connection and closes connections that exceed the +limits. The defaults are safe for typical clients (which authenticate before +sending document data); raise them only if you have a specific need. + +```js +const server = new Server({ + port: 1234, + + // Max bytes buffered across all documents of one unauthenticated connection. + maxUnauthenticatedQueueSize: 5 * 1024 * 1024, // default: 5 MiB + + // Max number of messages buffered for one unauthenticated connection. + maxUnauthenticatedQueueMessages: 1_000, // default: 1000 + + // Max number of documents one connection may open before authenticating any. + maxPendingDocuments: 100, // default: 100 + + // Connections that don't authenticate any document within `timeout` ms are + // closed. This deadline is not refreshed by inbound traffic. + timeout: 60_000, // default: 60s +}) +``` + +## Non-Node.js runtimes + +Use the `Hocuspocus` class directly to attach to any `WebSocketLike` instance (Bun, Deno, Cloudflare Workers, Express, etc.): + +```js +import { Hocuspocus } from "@hocuspocus/server" + +const hocuspocus = new Hocuspocus({ /* config */ }) + +// pass any WebSocket-like instance + request + optional context: +hocuspocus.handleConnection(ws, request, context) +``` + +## Documentation + +Full reference, hooks, extensions, and scaling guides: [tiptap.dev/docs/hocuspocus](https://tiptap.dev/docs/hocuspocus). + +## License + +MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..951150b --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,52 @@ +{ + "name": "@hocuspocus/server", + "description": "plug & play collaboration backend", + "version": "4.7.0", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "yjs", + "yjs-websocket", + "prosemirror" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-server.cjs", + "module": "dist/hocuspocus-server.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-server.esm.js", + "require": "./dist/hocuspocus-server.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@hocuspocus/common": "workspace:^", + "async-mutex": "^0.5.0", + "crossws": "^0.4.4", + "kleur": "^4.1.5", + "lib0": "^0.2.117" + }, + "peerDependencies": { + "y-protocols": "^1.0.6", + "yjs": "^13.6.8" + }, + "gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "engines": { + "node": ">=22" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/server/src/ClientConnection.ts b/packages/server/src/ClientConnection.ts new file mode 100644 index 0000000..9ac688a --- /dev/null +++ b/packages/server/src/ClientConnection.ts @@ -0,0 +1,641 @@ +import crypto from "node:crypto"; +import { + type CloseEvent, + ConnectionTimeout, + Forbidden, + ResetConnection, + Unauthorized, + WsReadyStates, +} from "@hocuspocus/common"; +import * as decoding from "lib0/decoding"; +import Connection from "./Connection.ts"; +import type Document from "./Document.ts"; +import type { Hocuspocus } from "./Hocuspocus.ts"; +import { IncomingMessage as SocketIncomingMessage } from "./IncomingMessage.ts"; +import { OutgoingMessage } from "./OutgoingMessage.ts"; +import type { + ConnectionConfiguration, + WebSocketLike, + afterHandleMessagePayload, + beforeHandleMessagePayload, + beforeSyncPayload, + onDisconnectPayload, +} from "./types.ts"; +import { MessageType } from "./types.ts"; +import { getParameters } from "./util/getParameters.ts"; + +/** + * The `ClientConnection` class is responsible for handling an incoming WebSocket + * + * TODO-refactor: + * - use event handlers instead of calling hooks directly, hooks should probably be called from Hocuspocus.ts + */ +export class ClientConnection { + // Map of established document connections, keyed by rawKey (composite or plain) + private readonly documentConnections: Record> = + {}; + + // While the connection will be establishing messages will + // be queued and handled later. + private readonly incomingMessageQueue: Record = {}; + + // Tracks the number of bytes buffered per document key, so the total amount + // of unauthenticated buffered data can be bounded (see GHSA-xwhh-v746-pj9m). + private readonly incomingMessageQueueBytes: Record = {}; + + // While the connection is establishing, keep track of which documents have received auth + private readonly documentConnectionsEstablished = new Set(); + + // Hook payloads keyed by rawKey (composite or plain) + private readonly hookPayloads: Record< + string, + { + instance: Hocuspocus; + request: Request; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; + context: Context; + providerVersion: string | null; + } + > = {}; + + private readonly callbacks = { + onClose: [(document: Document, payload: onDisconnectPayload) => {}], + }; + + // Every new connection gets a unique identifier. + private readonly socketId = crypto.randomUUID(); + + timeout: number; + + private readonly maxUnauthenticatedQueueSize: number; + + private readonly maxUnauthenticatedQueueMessages: number; + + private readonly maxPendingDocuments: number; + + pingInterval: NodeJS.Timeout; + + lastMessageReceivedAt = Date.now(); + + // When the connection was opened. Used to enforce a pre-authentication + // deadline that inbound traffic cannot refresh. + private readonly connectionEstablishedAt = Date.now(); + + // Set once any document on this connection has successfully authenticated. + // Sticky on purpose: once a client has proven itself, the normal idle-timeout + // logic applies for the rest of the socket's life, even if it later closes all + // of its documents. + private hasAuthenticated = false; + + // Set once the connection has been proactively torn down (timeout or a limit + // breach). Guards against processing — and re-logging — frames that were + // already in flight when we decided to close the socket. + private terminated = false; + + /** + * The `ClientConnection` class receives incoming WebSocket connections, + * runs all hooks: + * + * - onConnect for all connections + * - onAuthenticate only if required + * + * … and if nothings fails it'll fully establish the connection and + * load the Document then. + */ + constructor( + private readonly websocket: WebSocketLike, + private readonly request: Request, + private readonly documentProvider: { + createDocument: Hocuspocus["createDocument"]; + }, + // TODO: change to events + private readonly hooks: Hocuspocus["hooks"], + private readonly opts: { + timeout: number; + maxUnauthenticatedQueueSize?: number; + maxUnauthenticatedQueueMessages?: number; + maxPendingDocuments?: number; + }, + private readonly defaultContext: Context = {} as Context, + ) { + this.timeout = opts.timeout; + this.maxUnauthenticatedQueueSize = + opts.maxUnauthenticatedQueueSize ?? 5 * 1024 * 1024; + this.maxUnauthenticatedQueueMessages = + opts.maxUnauthenticatedQueueMessages ?? 1_000; + this.maxPendingDocuments = opts.maxPendingDocuments ?? 100; + this.pingInterval = setInterval(this.check, this.timeout); + } + + /** + * Handle WebSocket close event. Call this from your integration + * when the WebSocket connection closes. + */ + handleClose(event?: CloseEvent) { + this.close(event); + clearInterval(this.pingInterval); + } + + private close(event?: CloseEvent) { + Object.values(this.documentConnections).forEach((connection) => + connection.close(event), + ); + + // Release any buffered pre-authentication state. Without this, a + // timed-out or rejected connection would keep its queued messages and + // hook payloads in memory until the process exits (GHSA-xwhh-v746-pj9m). + for (const rawKey of Object.keys(this.incomingMessageQueue)) { + delete this.incomingMessageQueue[rawKey]; + delete this.incomingMessageQueueBytes[rawKey]; + } + for (const rawKey of Object.keys(this.hookPayloads)) { + delete this.hookPayloads[rawKey]; + } + this.documentConnectionsEstablished.clear(); + } + + /** + * Tear down the connection: release the established document connections and + * the buffered pre-authentication state, then close the underlying socket. + * Closing the socket is essential — otherwise an unauthenticated client can + * keep the socket (and its resources) open even after a timeout fires. + */ + private terminate(event?: CloseEvent) { + if (this.terminated) { + return; + } + this.terminated = true; + + this.close(event); + + if ( + this.websocket.readyState !== WsReadyStates.Closing && + this.websocket.readyState !== WsReadyStates.Closed + ) { + this.websocket.close(event?.code, event?.reason); + } + + clearInterval(this.pingInterval); + } + + /** + * Close the connection if no messages have been received within the timeout period. + * This replaces application-level ping/pong to maintain backward compatibility + * with older provider versions that don't understand Ping/Pong message types. + * Awareness updates (~every 30s) keep active connections alive. + */ + private check = () => { + // Until the connection has authenticated at least one document, use an + // absolute deadline based on when the connection opened. Inbound traffic + // updates `lastMessageReceivedAt`, so a flood of unauthenticated frames + // could otherwise refresh the timeout forever and keep the socket alive. + // Once authenticated, fall back to the normal idle behavior — even if the + // client later closes all of its documents — so an established client that + // keeps its socket open is not closed based on the original open time. + const referenceTime = this.hasAuthenticated + ? this.lastMessageReceivedAt + : this.connectionEstablishedAt; + + if (Date.now() - referenceTime > this.timeout) { + this.terminate(ConnectionTimeout); + } + }; + + // Total bytes currently buffered for unauthenticated documents on this + // connection. Derived on demand so the various cleanup paths only need to + // delete their keys without keeping a running counter in sync. + private getQueuedBytes(): number { + let total = 0; + for (const rawKey of Object.keys(this.incomingMessageQueueBytes)) { + total += this.incomingMessageQueueBytes[rawKey]; + } + return total; + } + + // Total number of messages currently buffered for unauthenticated documents. + private getQueuedMessageCount(): number { + let total = 0; + for (const rawKey of Object.keys(this.incomingMessageQueue)) { + total += this.incomingMessageQueue[rawKey].length; + } + return total; + } + + // Number of distinct documents on this connection that have not yet finished + // authenticating. Each holds its own queue and hook payload, so this is capped + // to prevent a single connection from amplifying memory usage by fanning out + // across many document names. We count by `isAuthenticated` (not by whether an + // Auth frame has been seen) so that sending an Auth frame first — which marks + // the document as "establishing" but keeps its hook payload alive while a slow + // `onAuthenticate` runs — cannot be used to bypass the limit. + private getPendingDocumentCount(): number { + let total = 0; + for (const rawKey of Object.keys(this.hookPayloads)) { + if (!this.hookPayloads[rawKey].connectionConfig.isAuthenticated) { + total += 1; + } + } + return total; + } + + /** + * Set a callback that will be triggered when the connection is closed + */ + public onClose( + callback: (document: Document, payload: onDisconnectPayload) => void, + ): ClientConnection { + this.callbacks.onClose.push(callback); + + return this; + } + + /** + * Create a new connection by the given request and document + */ + private createConnection( + connection: WebSocketLike, + document: Document, + hookPayload: (typeof this.hookPayloads)[string], + sessionId: string | null, + providerVersion: string | null, + ): Connection { + const instance = new Connection( + connection, + hookPayload.request, + document, + hookPayload.socketId, + hookPayload.context, + hookPayload.connectionConfig.readOnly, + sessionId, + providerVersion, + ); + + instance.onClose(async (document, event) => { + // Wait for any pending message processing to complete before running + // disconnect hooks. This ensures that document updates from queued messages + // are applied (and their debounced onStoreDocument scheduled) before the + // disconnect handler checks whether to call executeNow. + await instance.waitForPendingMessages(); + + const disconnectHookPayload: onDisconnectPayload = { + instance: this.documentProvider as Hocuspocus, // TODO, this will be removed when we use events instead of hooks for this class + clientsCount: document.getConnectionsCount(), + context: hookPayload.context, + document, + socketId: hookPayload.socketId, + documentName: document.name, + requestHeaders: hookPayload.request.headers, + requestParameters: getParameters(hookPayload.request), + }; + + await this.hooks("onDisconnect", disconnectHookPayload); + this.callbacks.onClose.forEach((callback) => + callback(document, disconnectHookPayload), + ); + }); + + instance.onStatelessCallback(async (payload) => { + try { + return await this.hooks("onStateless", payload); + } catch (error: any) { + if (error?.message) { + // if a hook rejects and the error is empty, do nothing + // this is only meant to prevent later hooks and the + // default handler to do something. if an error is present + // just rethrow it + throw error; + } + } + }); + + instance.beforeHandleMessage((connection, update) => { + const beforeHandleMessagePayload: beforeHandleMessagePayload = { + instance: this.documentProvider as Hocuspocus, // TODO, this will be removed when we use events instead of hooks for this class + clientsCount: document.getConnectionsCount(), + context: hookPayload.context, + document, + socketId: hookPayload.socketId, + connection, + documentName: document.name, + requestHeaders: hookPayload.request.headers, + requestParameters: getParameters(hookPayload.request), + update, + }; + + return this.hooks("beforeHandleMessage", beforeHandleMessagePayload); + }); + + instance.afterHandleMessage((connection, update) => { + const afterHandleMessagePayload: afterHandleMessagePayload = { + instance: this.documentProvider as Hocuspocus, + clientsCount: document.getConnectionsCount(), + context: hookPayload.context, + document, + socketId: hookPayload.socketId, + connection, + documentName: document.name, + requestHeaders: hookPayload.request.headers, + requestParameters: getParameters(hookPayload.request), + update, + }; + + return this.hooks("afterHandleMessage", afterHandleMessagePayload); + }); + + instance.beforeSync((connection, payload) => { + const beforeSyncPayload: beforeSyncPayload = { + clientsCount: document.getConnectionsCount(), + context: hookPayload.context, + document, + documentName: document.name, + connection, + type: payload.type, + payload: payload.payload, + }; + + return this.hooks("beforeSync", beforeSyncPayload); + }); + + return instance; + } + + // Once all hooks are run, we'll fully establish the connection: + private setUpNewConnection = async (rawKey: string, documentName: string, sessionId: string | null) => { + const hookPayload = this.hookPayloads[rawKey]; + // If no hook interrupts, create a document and connection + const document = await this.documentProvider.createDocument( + documentName, + hookPayload.request, + hookPayload.socketId, + hookPayload.connectionConfig, + hookPayload.context, + ); + const connection = this.createConnection(this.websocket, document, hookPayload, sessionId, hookPayload.providerVersion); + + connection.onClose((document, event) => { + delete this.hookPayloads[rawKey]; + delete this.documentConnections[rawKey]; + delete this.incomingMessageQueue[rawKey]; + delete this.incomingMessageQueueBytes[rawKey]; + this.documentConnectionsEstablished.delete(rawKey); + }); + + connection.onTokenSyncCallback(async (payload) => { + try { + return await this.hooks( + "onTokenSync", + { + ...hookPayload, + ...payload, + document, + connection, + documentName, + }, + (contextAdditions: Partial) => { + hookPayload.context = { + ...hookPayload.context, + ...contextAdditions, + }; + }, + ); + } catch (err: any) { + console.error(err); + const error = { ...Unauthorized, ...err }; + connection.close({ code: error.code, reason: error.reason }); + } + }); + + this.documentConnections[rawKey] = connection; + + // If the WebSocket has already disconnected (wow, that was fast) – then + // immediately call close to cleanup the connection and document in memory. + if ( + this.websocket.readyState === WsReadyStates.Closing || + this.websocket.readyState === WsReadyStates.Closed + ) { + this.close(); + return; + } + + // Drain queued messages to the Connection. Once drained, the buffered + // data is no longer needed (subsequent messages route straight to the + // Connection), so release it and stop counting it against the queue + // limits. + this.incomingMessageQueue[rawKey]?.forEach((input) => { + connection.handleMessage(input); + }); + delete this.incomingMessageQueue[rawKey]; + delete this.incomingMessageQueueBytes[rawKey]; + + await this.hooks("connected", { + ...hookPayload, + documentName, + context: hookPayload.context, + connection, + }); + }; + + // This listener handles authentication messages and queues everything else. + private handleQueueingMessage = async (data: Uint8Array, rawKey: string, documentName: string) => { + try { + const tmpMsg = new SocketIncomingMessage(data); + + decoding.readVarString(tmpMsg.decoder); // skip the message address (already extracted) + const type = decoding.readVarUint(tmpMsg.decoder); + + if ( + !( + type === MessageType.Auth && + !this.documentConnectionsEstablished.has(rawKey) + ) + ) { + // Bound the amount of data buffered before authentication. An + // unauthenticated client must not be able to grow this queue without + // limit (GHSA-xwhh-v746-pj9m). + const queuedBytes = this.getQueuedBytes(); + const queuedMessages = this.getQueuedMessageCount(); + const exceedsBytes = + queuedBytes + data.byteLength > this.maxUnauthenticatedQueueSize; + const exceedsMessages = + queuedMessages + 1 > this.maxUnauthenticatedQueueMessages; + + if (exceedsBytes || exceedsMessages) { + console.warn( + exceedsBytes + ? `Hocuspocus: closing connection ${this.socketId}: unauthenticated message queue size limit exceeded for "${documentName}" (${queuedBytes + data.byteLength} > maxUnauthenticatedQueueSize ${this.maxUnauthenticatedQueueSize} bytes).` + : `Hocuspocus: closing connection ${this.socketId}: unauthenticated message queue length limit exceeded for "${documentName}" (${queuedMessages + 1} > maxUnauthenticatedQueueMessages ${this.maxUnauthenticatedQueueMessages}).`, + ); + this.terminate(ResetConnection); + return; + } + + this.incomingMessageQueue[rawKey].push(data); + this.incomingMessageQueueBytes[rawKey] = + (this.incomingMessageQueueBytes[rawKey] ?? 0) + data.byteLength; + return; + } + + // Okay, we've got the authentication message we're waiting for: + this.documentConnectionsEstablished.add(rawKey); + + // The 2nd integer contains the submessage type + // which will always be authentication when sent from client -> server + decoding.readVarUint(tmpMsg.decoder); + const token = decoding.readVarString(tmpMsg.decoder); + + // Try to read providerVersion (new protocol field) + let providerVersion: string | null = null; + if (decoding.hasContent(tmpMsg.decoder)) { + providerVersion = decoding.readVarString(tmpMsg.decoder); + } + + // Extract sessionId from the rawKey (documentName\0sessionId) if present + const sepIdx = rawKey.indexOf('\0'); + const sessionId = sepIdx === -1 ? null : rawKey.substring(sepIdx + 1); + + // Response uses rawKey so session-aware clients can route correctly + const responseAddress = rawKey; + + try { + const hookPayload = this.hookPayloads[rawKey]; + hookPayload.providerVersion = providerVersion; + + await this.hooks( + "onConnect", + { ...hookPayload, documentName }, + (contextAdditions: Partial) => { + hookPayload.context = { + ...hookPayload.context, + ...contextAdditions, + }; + }, + ); + + await this.hooks( + "onAuthenticate", + { + token, + ...hookPayload, + documentName, + }, + (contextAdditions: Partial) => { + hookPayload.context = { + ...hookPayload.context, + ...contextAdditions, + }; + }, + ); + // All `onAuthenticate` hooks passed. + hookPayload.connectionConfig.isAuthenticated = true; + this.hasAuthenticated = true; + + // Let the client know that authentication was successful. + const message = new OutgoingMessage(responseAddress).writeAuthenticated( + hookPayload.connectionConfig.readOnly, + ); + + this.websocket.send(message.toUint8Array()); + + // Time to actually establish the connection. + await this.setUpNewConnection(rawKey, documentName, sessionId); + } catch (err: any) { + const error = err || Forbidden; + const message = new OutgoingMessage(responseAddress).writePermissionDenied( + error.reason ?? "permission-denied", + ); + + this.websocket.send(message.toUint8Array()); + + // Clean up all state for this document so a retry is treated + // as a fresh first connection attempt. + this.documentConnectionsEstablished.delete(rawKey); + delete this.hookPayloads[rawKey]; + delete this.incomingMessageQueue[rawKey]; + delete this.incomingMessageQueueBytes[rawKey]; + } + + // Catch errors due to failed decoding of data + } catch (error) { + console.error(error); + this.websocket.close(ResetConnection.code, ResetConnection.reason); + } + }; + + /** + * Handle an incoming WebSocket message. Call this from your integration + * when the WebSocket receives a binary message. + */ + handleMessage = (data: Uint8Array) => { + // Ignore anything that arrives after we've decided to tear the connection + // down (e.g. frames that were already in flight when a limit was breached). + if (this.terminated) { + return; + } + + this.lastMessageReceivedAt = Date.now(); + + try { + const tmpMsg = new SocketIncomingMessage(data); + + const rawKey = decoding.readVarString(tmpMsg.decoder); + + // Extract the plain documentName (the raw key may be documentName\0sessionId) + const sepIdx = rawKey.indexOf('\0'); + const documentName = sepIdx === -1 ? rawKey : rawKey.substring(0, sepIdx); + + // Look up by rawKey first (session-aware providers), then fall back + // to plain documentName for backward compatibility with old providers + const connection = this.documentConnections[rawKey] + ?? this.documentConnections[documentName]; + if (connection) { + connection.handleMessage(data); + return; + } + + const isFirst = this.incomingMessageQueue[rawKey] === undefined; + if (isFirst) { + // Cap the number of documents a single connection can open before + // authenticating any of them. Each pending document allocates its own + // queue and hook payload, so this prevents memory amplification by + // fanning out across many document names (GHSA-xwhh-v746-pj9m). + if (this.getPendingDocumentCount() >= this.maxPendingDocuments) { + console.warn( + `Hocuspocus: closing connection ${this.socketId}: too many pending unauthenticated documents (maxPendingDocuments ${this.maxPendingDocuments}). Last requested document: "${documentName}".`, + ); + this.terminate(ResetConnection); + return; + } + + this.incomingMessageQueue[rawKey] = []; + if (this.hookPayloads[rawKey]) { + throw new Error("first message, but hookPayloads exists"); + } + + this.hookPayloads[rawKey] = { + instance: this.documentProvider as Hocuspocus, + request: this.request, + connectionConfig: { + readOnly: false, + isAuthenticated: false, + }, + requestHeaders: this.request.headers, + requestParameters: getParameters(this.request), + socketId: this.socketId, + context: { + ...this.defaultContext, + }, + providerVersion: null as string | null, + }; + } + + this.handleQueueingMessage(data, rawKey, documentName); + } catch (closeError) { + // catch is needed in case an invalid payload crashes the parsing of the Uint8Array + console.error(closeError); + this.websocket.close(Unauthorized.code, Unauthorized.reason); + } + }; +} diff --git a/packages/server/src/Connection.ts b/packages/server/src/Connection.ts new file mode 100644 index 0000000..1981acb --- /dev/null +++ b/packages/server/src/Connection.ts @@ -0,0 +1,305 @@ +import { + type CloseEvent, + ResetConnection, + WsReadyStates, +} from "@hocuspocus/common"; +import type Document from "./Document.ts"; +import { IncomingMessage } from "./IncomingMessage.ts"; +import { MessageReceiver } from "./MessageReceiver.ts"; +import { OutgoingMessage } from "./OutgoingMessage.ts"; +import type { + WebSocketLike, + beforeSyncPayload, + onStatelessPayload, +} from "./types.ts"; + +export class Connection { + webSocket: WebSocketLike; + + context: Context; + + document: Document; + + request: Request; + + callbacks = { + onClose: [(document: Document, event?: CloseEvent) => {}], + beforeHandleMessage: (connection: Connection, update: Uint8Array) => + Promise.resolve(), + afterHandleMessage: (connection: Connection, update: Uint8Array) => + Promise.resolve(), + beforeSync: ( + connection: Connection, + payload: Pick, + ) => Promise.resolve(), + statelessCallback: (payload: onStatelessPayload) => Promise.resolve(), + onTokenSyncCallback: (payload: { token: string }) => Promise.resolve(), + }; + + socketId: string; + + readOnly: boolean; + + sessionId: string | null; + + providerVersion: string | null; + + /** + * The address string prefixed to outgoing messages. + * Session-aware clients get `documentName\0sessionId`; legacy clients get plain `documentName`. + */ + get messageAddress(): string { + return this.sessionId + ? `${this.document.name}\0${this.sessionId}` + : this.document.name; + } + + private messageQueue: Uint8Array[] = []; + + private processingPromise: Promise = Promise.resolve(); + + /** + * Constructor. + */ + constructor( + connection: WebSocketLike, + request: Request, + document: Document, + socketId: string, + context: Context, + readOnly = false, + sessionId?: string | null, + providerVersion?: string | null, + ) { + this.webSocket = connection; + this.context = context; + this.document = document; + this.request = request; + this.socketId = socketId; + this.readOnly = readOnly; + this.sessionId = sessionId ?? null; + this.providerVersion = providerVersion ?? null; + + this.document.addConnection(this); + + this.sendCurrentAwareness(); + } + + /** + * Set a callback that will be triggered when the connection is closed + */ + onClose( + callback: (document: Document, event?: CloseEvent) => void, + ): Connection { + this.callbacks.onClose.push(callback); + + return this; + } + + /** + * Set a callback that will be triggered when an stateless message is received + */ + onStatelessCallback( + callback: (payload: onStatelessPayload) => Promise, + ): Connection { + this.callbacks.statelessCallback = callback; + + return this; + } + + /** + * Set a callback that will be triggered before an message is handled + */ + beforeHandleMessage( + callback: (connection: Connection, update: Uint8Array) => Promise, + ): Connection { + this.callbacks.beforeHandleMessage = callback; + + return this; + } + + /** + * Set a callback that will be triggered after a message has been handled + */ + afterHandleMessage( + callback: (connection: Connection, update: Uint8Array) => Promise, + ): Connection { + this.callbacks.afterHandleMessage = callback; + + return this; + } + + /** + * Set a callback that will be triggered before a sync message is handled + */ + beforeSync( + callback: ( + connection: Connection, + payload: Pick, + ) => Promise, + ): Connection { + this.callbacks.beforeSync = callback; + + return this; + } + + /** + * Set a callback that will be triggered when on token sync message is received + */ + onTokenSyncCallback( + callback: (payload: { token: string }) => Promise, + ): Connection { + this.callbacks.onTokenSyncCallback = callback; + + return this; + } + + /** + * Returns a promise that resolves when all queued messages have been processed. + */ + waitForPendingMessages(): Promise { + return this.processingPromise; + } + + /** + * Send the given message + */ + send(message: Uint8Array): void { + if ( + this.webSocket.readyState === WsReadyStates.Closing || + this.webSocket.readyState === WsReadyStates.Closed + ) { + this.close(); + return; + } + + try { + this.webSocket.send(message); + } catch (exception) { + this.close(); + } + } + + /** + * Send a stateless message with payload + */ + public sendStateless(payload: string): void { + const message = new OutgoingMessage(this.messageAddress).writeStateless( + payload, + ); + + this.send(message.toUint8Array()); + } + + /** + * Request current token from the client + */ + public requestToken(): void { + const message = new OutgoingMessage( + this.messageAddress, + ).writeTokenSyncRequest(); + + this.send(message.toUint8Array()); + } + + /** + * Graceful wrapper around the WebSocket close method. + */ + close(event?: CloseEvent): void { + if (this.document.hasConnection(this)) { + this.document.removeConnection(this); + this.callbacks.onClose.forEach( + (callback: (arg0: Document, arg1?: CloseEvent) => any) => + callback(this.document, event), + ); + + const closeMessage = new OutgoingMessage(this.messageAddress); + closeMessage.writeCloseMessage( + event?.reason ?? "Server closed the connection", + ); + this.send(closeMessage.toUint8Array()); + } + } + + /** + * Send the current document awareness to the client, if any + * @private + */ + private sendCurrentAwareness(): void { + if (!this.document.hasAwarenessStates()) { + return; + } + + const awarenessMessage = new OutgoingMessage( + this.messageAddress, + ).createAwarenessUpdateMessage(this.document.awareness); + + this.send(awarenessMessage.toUint8Array()); + } + + /** + * Handle an incoming message + * @public + */ + public handleMessage(data: Uint8Array): void { + this.messageQueue.push(data); + + if (this.messageQueue.length === 1) { + this.processingPromise = this.processMessages(); + } + } + + private async processMessages() { + while (this.messageQueue.length > 0) { + const rawUpdate = this.messageQueue.at(0) as Uint8Array; + + const message = new IncomingMessage(rawUpdate); + const rawKey = message.readVarString(); + + // Accept messages addressed with either the plain documentName or documentName\0sessionId + const sepIdx = rawKey.indexOf('\0'); + const documentName = sepIdx === -1 ? rawKey : rawKey.substring(0, sepIdx); + if (documentName !== this.document.name) { + this.messageQueue.shift(); + continue; + } + + // Write the correct address so replies reach the right provider + message.writeVarString(this.messageAddress); + + try { + await this.callbacks.beforeHandleMessage(this, rawUpdate); + const receiver = new MessageReceiver(message); + + try { + await receiver.apply(this.document, this); + } finally { + try { + await this.callbacks.afterHandleMessage(this, rawUpdate); + } catch (afterError) { + console.error( + `afterHandleMessage hook failed (while handling ${documentName})`, + afterError, + ); + } + } + // biome-ignore lint/suspicious/noExplicitAny: + } catch (e: any) { + console.error( + `closing connection ${this.socketId} (while handling ${documentName}) because of exception`, + e, + ); + this.close({ + code: "code" in e && typeof e.code === 'number' ? e.code : ResetConnection.code, + reason: "reason" in e ? e.reason : ResetConnection.reason, + }); + this.messageQueue.length = 0; + return; + } + + this.messageQueue.shift(); + } + } +} + +export default Connection; diff --git a/packages/server/src/DirectConnection.ts b/packages/server/src/DirectConnection.ts new file mode 100644 index 0000000..4ebe381 --- /dev/null +++ b/packages/server/src/DirectConnection.ts @@ -0,0 +1,103 @@ +import { URLSearchParams } from "node:url"; +import type Document from "./Document.ts"; +import type { Hocuspocus } from "./Hocuspocus.ts"; +import type { + DirectConnection as DirectConnectionInterface, + DisconnectOptions, + LocalTransactionOrigin, +} from "./types.ts"; + +export class DirectConnection + implements DirectConnectionInterface +{ + document: Document | null = null; + + instance!: Hocuspocus; + + context: Context; + + /** + * Constructor. + */ + constructor(document: Document, instance: Hocuspocus, context?: Context) { + this.document = document; + this.instance = instance; + this.context = (context ?? {}) as Context; + + this.document.addDirectConnection(); + } + + async transact(transaction: (document: Document) => void) { + if (!this.document) { + throw new Error("direct connection closed"); + } + + this.document.transact( + (x) => { + // biome-ignore lint/style/noNonNullAssertion: + transaction(this.document!); + }, + { + source: "local", + context: this.context, + } satisfies LocalTransactionOrigin, + ); + } + + async disconnect(options?: DisconnectOptions) { + if (this.document) { + // Defaults to true regardless of the server-wide `unloadImmediately` + // setting, so the historical "durable on disconnect" behavior is kept + // unless a caller explicitly opts into keeping the document warm. + const unloadImmediately = options?.unloadImmediately ?? true; + + this.document?.removeDirectConnection(); + + // With unloadImmediately the document is persisted synchronously. + // Otherwise the store is debounced, so the document stays warm in memory + // and a follow-up direct connection can reuse it (coalescing writes), + // mirroring how websocket connections behave on close. + await this.instance.storeDocumentHooks( + this.document, + { + clientsCount: this.document.getConnectionsCount(), + lastContext: this.context, + lastTransactionOrigin: { + source: "local", + context: this.context, + } satisfies LocalTransactionOrigin, + document: this.document, + documentName: this.document.name, + instance: this.instance, + }, + unloadImmediately, + ); + + // If the direct connection was the only connection to the document + // then we should trigger the onDisconnect hook for this doc. We only + // unload it right away when unloadImmediately is set; otherwise the + // debounced store above unloads it once it has flushed. + if ( + this.document.getConnectionsCount() === 0 && + !this.document.saveMutex.isLocked() + ) { + await this.instance.hooks("onDisconnect", { + instance: this.instance, + clientsCount: this.document.getConnectionsCount(), + context: this.context, + document: this.document, + socketId: "server", + documentName: this.document.name, + requestHeaders: new Headers(), + requestParameters: new URLSearchParams(), + }); + + if (unloadImmediately) { + await this.instance.unloadDocument(this.document); + } + } + + this.document = null; + } + } +} diff --git a/packages/server/src/Document.ts b/packages/server/src/Document.ts new file mode 100644 index 0000000..9fa5f85 --- /dev/null +++ b/packages/server/src/Document.ts @@ -0,0 +1,487 @@ +import { Mutex } from "async-mutex"; +import { + Awareness, + applyAwarenessUpdate, + removeAwarenessStates, +} from "y-protocols/awareness"; +import { applyUpdate, Doc, encodeStateAsUpdate, mergeUpdates } from "yjs"; +import type Connection from "./Connection.ts"; +import { OutgoingMessage } from "./OutgoingMessage.ts"; +import type { + AwarenessUpdate, + ConnectionTransactionOrigin, + FlushOptions, + TransactionOrigin, +} from "./types.ts"; +import { isTransactionOrigin } from "./types.ts"; + +export class Document extends Doc { + awareness: Awareness; + + callbacks = { + // eslint-disable-next-line @typescript-eslint/no-empty-function + onUpdate: (document: Document, origin: unknown, update: Uint8Array) => {}, + beforeBroadcastStateless: (document: Document, stateless: string) => {}, + beforeHandleAwareness: ( + document: Document, + states: Map>, + transactionOrigin: unknown, + ) => Promise.resolve(), + }; + + connections: Map< + Connection, + { + clients: Set; + } + > = new Map(); + + // The number of direct (non-websocket) connections to this document + directConnectionsCount = 0; + + name: string; + + isLoading: boolean; + + isDestroyed = false; + + saveMutex = new Mutex(); + + lastChangeTime = 0; + + /** + * Batch outgoing broadcasts over this window (ms), or false to send each one + * immediately. See `scheduleFlush`. + */ + flushDelay: FlushOptions["flushDelay"]; + + /** + * Flush early once this many bytes of document updates are buffered. + */ + flushMaxBytes: FlushOptions["flushMaxBytes"]; + + private pendingUpdates: Array = []; + + private pendingUpdateBytes = 0; + + private pendingAwarenessClients: Set = new Set(); + + private cancelFlush: (() => void) | null = null; + + /** + * Constructor. + */ + constructor( + name: string, + yDocOptions?: object, + options?: Partial, + ) { + super(yDocOptions); + + this.name = name; + this.flushDelay = options?.flushDelay ?? 0; + this.flushMaxBytes = options?.flushMaxBytes ?? 1024 * 1024; + + this.awareness = new Awareness(this); + this.awareness.setLocalState(null); + + this.awareness.on("update", this.handleAwarenessUpdate.bind(this)); + this.on("update", this.handleUpdate.bind(this)); + + this.isLoading = true; + } + + /** + * Check if the Document (XMLFragment or Map) is empty + */ + isEmpty(fieldName: string): boolean { + return !this.get(fieldName)._start && !this.get(fieldName)._map.size; + } + + /** + * Merge the given document(s) into this one + */ + merge(documents: Doc | Array): Document { + (Array.isArray(documents) ? documents : [documents]).forEach((document) => { + applyUpdate(this, encodeStateAsUpdate(document)); + }); + + return this; + } + + /** + * Set a callback that will be triggered when the document is updated + */ + onUpdate( + callback: (document: Document, origin: unknown, update: Uint8Array) => void, + ): Document { + this.callbacks.onUpdate = callback; + + return this; + } + + /** + * Set a callback that will be triggered before a stateless message is broadcasted + */ + beforeBroadcastStateless( + callback: (document: Document, stateless: string) => void, + ): Document { + this.callbacks.beforeBroadcastStateless = callback; + + return this; + } + + /** + * Set a callback that will be triggered before an inbound awareness update + * is applied to this document's awareness state. The callback receives the + * document, the decoded per-client states as a mutable `Map`, and the + * `TransactionOrigin` that will be forwarded to `applyAwarenessUpdate`. + * Use `isTransactionOrigin(origin)` to discriminate sources. Mutate the + * map in place (set/delete/field changes) to rewrite the update, or throw + * to reject it entirely. + */ + beforeHandleAwareness( + callback: ( + document: Document, + states: Map>, + transactionOrigin: unknown, + ) => Promise, + ): Document { + this.callbacks.beforeHandleAwareness = callback; + + return this; + } + + /** + * Register a connection and a set of clients on this document keyed by the + * underlying websocket connection + */ + addConnection(connection: Connection): Document { + this.connections.set(connection, { + clients: new Set(), + }); + + return this; + } + + /** + * Is the given connection registered on this document + */ + hasConnection(connection: Connection): boolean { + return this.connections.has(connection); + } + + /** + * Remove the given connection from this document + */ + removeConnection(connection: Connection): Document { + const entry = this.connections.get(connection); + if (entry) { + removeAwarenessStates(this.awareness, Array.from(entry.clients), null); + } + + this.connections.delete(connection); + + return this; + } + + addDirectConnection(): Document { + this.directConnectionsCount += 1; + + return this; + } + + removeDirectConnection(): Document { + if (this.directConnectionsCount > 0) { + this.directConnectionsCount -= 1; + } + + return this; + } + + /** + * Get the number of active connections for this document + */ + getConnectionsCount(): number { + return this.connections.size + this.directConnectionsCount; + } + + /** + * Get an array of registered connections + */ + getConnections(): Array { + return Array.from(this.connections.keys()); + } + + /** + * Get the client ids for the given connection instance + */ + getClients(connection: Connection): Set { + const entry = this.connections.get(connection); + + return entry?.clients === undefined ? new Set() : entry.clients; + } + + /** + * Has the document awareness states + */ + hasAwarenessStates(): boolean { + return this.awareness.getStates().size > 0; + } + + /** + * Apply the given awareness update + */ + applyAwarenessUpdate(connection: Connection, update: Uint8Array): Document { + applyAwarenessUpdate(this.awareness, update, { + source: "connection", + connection, + } satisfies ConnectionTransactionOrigin); + + return this; + } + + /** + * Handle an awareness update and sync changes to clients + * @private + */ + private handleAwarenessUpdate( + { added, updated, removed }: AwarenessUpdate, + origin: TransactionOrigin | null, + ): Document { + const changedClients = added.concat(updated, removed); + + const originConnection: Connection | null = + isTransactionOrigin(origin) && origin.source === "connection" + ? origin.connection + : null; + + if (originConnection !== null) { + const entry = this.connections.get(originConnection); + + if (entry) { + added.forEach((clientId: any) => entry.clients.add(clientId)); + removed.forEach((clientId: any) => entry.clients.delete(clientId)); + } + } + + if (this.flushDelay === false) { + this.broadcastAwarenessUpdate(changedClients); + + return this; + } + + for (const clientId of changedClients) { + this.pendingAwarenessClients.add(clientId); + } + + this.scheduleFlush(); + + return this; + } + + /** + * Send one message to every connection, encoding it once per run of + * connections that share a message address. + * + * Connections that share a message address receive byte-identical payloads, + * so consecutive connections with the same address reuse one buffer instead + * of re-encoding per connection. With session awareness disabled (the + * default) every connection on a document shares the plain document name, + * which collapses the whole broadcast to a single encode. + * + * A one-entry cache rather than a Map keyed by address: it makes the common + * cases optimal (all addresses equal, or all distinct) at no cost, and only + * gives up dedup when addresses interleave, where it still matches the + * previous encode-per-connection behaviour. + */ + private broadcast( + encode: (messageAddress: string) => Uint8Array, + filter?: (connection: Connection) => boolean, + ): void { + let cachedAddress: string | undefined; + let cachedMessage: Uint8Array | undefined; + + for (const connection of this.getConnections()) { + if (filter !== undefined && !filter(connection)) { + continue; + } + + const address = connection.messageAddress; + + if (cachedMessage === undefined || address !== cachedAddress) { + cachedAddress = address; + cachedMessage = encode(address); + } + + connection.send(cachedMessage); + } + } + + private broadcastAwarenessUpdate(changedClients: Array): void { + this.broadcast((address) => + new OutgoingMessage(address) + .createAwarenessUpdateMessage(this.awareness, changedClients) + .toUint8Array(), + ); + } + + /** + * Handle an updated document and sync changes to clients + */ + private handleUpdate(update: Uint8Array, origin: unknown): Document { + // Stays synchronous even when broadcasts are batched: this drives + // `onChange`, the `onStoreDocument` debounce and the Redis publish in + // extension-redis. Deferring it would change persistence and cluster + // sync timing, not just the wire traffic. + this.callbacks.onUpdate(this, origin, update); + + if (this.flushDelay === false) { + this.broadcastUpdate(update); + + return this; + } + + this.pendingUpdates.push(update); + this.pendingUpdateBytes += update.length; + + if (this.pendingUpdateBytes >= this.flushMaxBytes) { + this.flush(); + } else { + this.scheduleFlush(); + } + + return this; + } + + private broadcastUpdate(update: Uint8Array): void { + this.broadcast((address) => + new OutgoingMessage(address) + .createSyncMessage() + .writeUpdate(update) + .toUint8Array(), + ); + } + + /** + * Fixed-window batching: the first buffered change starts the window and + * everything until it closes goes out together, so the added latency stays + * capped at `flushDelay` instead of growing while clients keep typing. + * + * `flushDelay: 0` means "coalesce whatever arrives in this event loop turn". + * setImmediate runs right after the current poll phase, so a burst of socket + * reads is merged without adding any delay. `setTimeout(…, 0)` would not do + * that: it clamps to 1ms and runs in the timers phase. + */ + private scheduleFlush(): void { + if (this.cancelFlush !== null) { + return; + } + + const run = () => { + this.cancelFlush = null; + this.flush(); + }; + + if (this.flushDelay === 0 && typeof setImmediate === "function") { + const handle = setImmediate(run); + + this.cancelFlush = () => clearImmediate(handle); + + return; + } + + const handle = setTimeout(run, this.flushDelay as number); + + this.cancelFlush = () => clearTimeout(handle); + } + + /** + * Send everything buffered for the current flush window right away. Document + * updates are merged into a single message; awareness collapses to the latest + * state of each changed client, so an add and a removal within one window + * resolve to the correct end state rather than both being sent. + * + * Safe to call when nothing is pending. + */ + public flush(): Document { + if (this.cancelFlush !== null) { + this.cancelFlush(); + this.cancelFlush = null; + } + + if (this.pendingUpdates.length > 0) { + const update = + this.pendingUpdates.length === 1 + ? this.pendingUpdates[0] + : mergeUpdates(this.pendingUpdates); + + this.pendingUpdates = []; + this.pendingUpdateBytes = 0; + + this.broadcastUpdate(update); + } + + if (this.pendingAwarenessClients.size > 0) { + const clients = Array.from(this.pendingAwarenessClients); + + this.pendingAwarenessClients.clear(); + + this.broadcastAwarenessUpdate(clients); + } + + return this; + } + + /** + * Broadcast stateless message to all connections + * + * Shares one encoded buffer per message address like the update and awareness + * broadcasts. Not batched, though: stateless payloads are opaque discrete + * events, so unlike updates they cannot be merged and unlike awareness they + * do not collapse to a latest state — deferring them would delay delivery + * without reducing the number of messages. + */ + public broadcastStateless( + payload: string, + filter?: (conn: Connection) => boolean, + ): void { + this.callbacks.beforeBroadcastStateless(this, payload); + + this.relayStateless(payload, filter); + } + + /** + * Send a stateless payload to this instance's connections *without* running + * the `beforeBroadcastStateless` hook. + * + * For relaying a payload that another server instance already broadcast: the + * hook ran there, and running it again here would fire it once per instance. + * Prefer `broadcastStateless` unless you are that relay. + */ + public relayStateless( + payload: string, + filter?: (conn: Connection) => boolean, + ): void { + this.broadcast( + (address) => + new OutgoingMessage(address).writeStateless(payload).toUint8Array(), + filter, + ); + } + + destroy() { + // Send what is still buffered instead of dropping it, then stop batching: + // `super.destroy()` tears the awareness down, which broadcasts one last + // time, and that has to reach clients here just as it does on a document + // that never batched. Usually a no-op, because a document is only + // unloaded once its last connection is gone. + this.flush(); + this.flushDelay = false; + + super.destroy(); + this.isDestroyed = true; + } +} + +export default Document; diff --git a/packages/server/src/Hocuspocus.ts b/packages/server/src/Hocuspocus.ts new file mode 100644 index 0000000..1dab71f --- /dev/null +++ b/packages/server/src/Hocuspocus.ts @@ -0,0 +1,665 @@ +import crypto from "node:crypto"; +import { awarenessStatesToArray, ResetConnection, SkipFurtherHooksError } from "@hocuspocus/common"; +import { applyUpdate, Doc, encodeStateAsUpdate } from "yjs"; +import meta from "../package.json" with { type: "json" }; + +import { ClientConnection } from "./ClientConnection.ts"; +import { DirectConnection } from "./DirectConnection.ts"; +import Document from "./Document.ts"; +import type { Server } from "./Server.ts"; +import type { + AwarenessUpdate, + beforeBroadcastStatelessPayload, + Configuration, + ConnectionConfiguration, + HookName, + HookPayloadByName, + onChangePayload, + onDisconnectPayload, + onStoreDocumentPayload, + WebSocketLike, +} from "./types.ts"; +import { isTransactionOrigin, shouldSkipStoreHooks } from "./types.ts"; +import { useDebounce } from "./util/debounce.ts"; +import { getParameters } from "./util/getParameters.ts"; + +export const defaultConfiguration = { + name: null, + timeout: 60_000, + debounce: 2_000, + maxDebounce: 10_000, + quiet: false, + yDocOptions: { + gc: true, + gcFilter: () => true, + }, + unloadImmediately: true, + maxUnauthenticatedQueueSize: 5 * 1024 * 1024, + maxUnauthenticatedQueueMessages: 1_000, + maxPendingDocuments: 100, + flushDelay: 0 as false | number, + flushMaxBytes: 1024 * 1024, +}; + +export class Hocuspocus { + configuration: Configuration = { + ...defaultConfiguration, + extensions: [], + onConfigure: () => new Promise((r) => r(null)), + onListen: () => new Promise((r) => r(null)), + onUpgrade: () => new Promise((r) => r(null)), + onConnect: () => new Promise((r) => r(null)), + connected: () => new Promise((r) => r(null)), + beforeHandleMessage: () => new Promise((r) => r(null)), + afterHandleMessage: () => new Promise((r) => r(null)), + beforeHandleAwareness: () => new Promise((r) => r()), + beforeSync: () => new Promise((r) => r(null)), + beforeBroadcastStateless: () => new Promise((r) => r(null)), + onStateless: () => new Promise((r) => r(null)), + onChange: () => new Promise((r) => r(null)), + onCreateDocument: () => new Promise((r) => r(null)), + onLoadDocument: () => new Promise((r) => r(null)), + onStoreDocument: () => new Promise((r) => r(null)), + afterStoreDocument: () => new Promise((r) => r(null)), + onAwarenessUpdate: () => new Promise((r) => r(null)), + onRequest: () => new Promise((r) => r(null)), + onDisconnect: () => new Promise((r) => r(null)), + onDestroy: () => new Promise((r) => r(null)), + }; + + loadingDocuments: Map> = new Map(); + unloadingDocuments: Map> = new Map(); + + documents: Map = new Map(); + + server?: Server; + + debouncer = useDebounce(); + + constructor(configuration?: Partial>) { + if (configuration) { + this.configure(configuration); + } + } + + /** + * Configure Hocuspocus + */ + configure( + configuration: Partial>, + ): Hocuspocus { + this.configuration = { + ...this.configuration, + ...configuration, + }; + + this.configuration.extensions.sort((a, b) => { + const one = typeof a.priority === "undefined" ? 100 : a.priority; + const two = typeof b.priority === "undefined" ? 100 : b.priority; + + if (one > two) { + return -1; + } + + if (one < two) { + return 1; + } + + return 0; + }); + + this.configuration.extensions.push({ + onConfigure: this.configuration.onConfigure, + onListen: this.configuration.onListen, + onUpgrade: this.configuration.onUpgrade, + onConnect: this.configuration.onConnect, + connected: this.configuration.connected, + onAuthenticate: this.configuration.onAuthenticate, + onTokenSync: this.configuration.onTokenSync, + onLoadDocument: this.configuration.onLoadDocument, + afterLoadDocument: this.configuration.afterLoadDocument, + beforeHandleMessage: this.configuration.beforeHandleMessage, + afterHandleMessage: this.configuration.afterHandleMessage, + beforeHandleAwareness: this.configuration.beforeHandleAwareness, + beforeBroadcastStateless: this.configuration.beforeBroadcastStateless, + beforeSync: this.configuration.beforeSync, + onStateless: this.configuration.onStateless, + onChange: this.configuration.onChange, + onStoreDocument: this.configuration.onStoreDocument, + afterStoreDocument: this.configuration.afterStoreDocument, + onAwarenessUpdate: this.configuration.onAwarenessUpdate, + onRequest: this.configuration.onRequest, + beforeUnloadDocument: this.configuration.beforeUnloadDocument, + afterUnloadDocument: this.configuration.afterUnloadDocument, + onDisconnect: this.configuration.onDisconnect, + onDestroy: this.configuration.onDestroy, + }); + + this.hooks("onConfigure", { + configuration: this.configuration, + version: meta.version, + instance: this, + }); + + return this; + } + + /** + * Get the total number of active documents + */ + getDocumentsCount(): number { + return this.documents.size; + } + + /** + * Get the total number of active connections + */ + getConnectionsCount(): number { + const uniqueSocketIds = new Set(); + const totalDirectConnections = Array.from(this.documents.values()).reduce( + (acc, document) => { + // Accumulate unique socket IDs + document.getConnections().forEach(({ socketId }) => { + uniqueSocketIds.add(socketId); + }); + // Accumulate direct connections + return acc + document.directConnectionsCount; + }, + 0, + ); + // Return the sum of unique socket IDs and direct connections + return uniqueSocketIds.size + totalDirectConnections; + } + + /** + * Immediately execute all pending debounced onStoreDocument calls. + * Useful during shutdown to ensure documents are persisted and unloaded + * before the server exits, even when unloadImmediately is false. + */ + flushPendingStores() { + this.documents.forEach((document: Document) => { + const debounceId = `onStoreDocument-${document.name}`; + if (!document.isLoading && this.debouncer.isDebounced(debounceId)) { + this.debouncer.executeNow(debounceId); + } + }); + } + + /** + * Force close one or more connections + */ + closeConnections(documentName?: string) { + // Iterate through all connections for all documents + // and invoke their close method, which is a graceful + // disconnect wrapper around the underlying websocket.close + this.documents.forEach((document: Document) => { + // If a documentName was specified, bail if it doesn't match + if (documentName && document.name !== documentName) { + return; + } + + document.connections.forEach((_clients, connection) => { + connection.close(ResetConnection); + }); + }); + } + + /** + * The `handleConnection` method receives incoming WebSocket connections, + * runs all hooks: + * + * - onConnect for all connections + * - onAuthenticate only if required + * + * … and if nothing fails it'll fully establish the connection and + * load the Document then. + */ + handleConnection( + incoming: WebSocket | WebSocketLike, + request: Request, + defaultContext: Context = {} as Context, + ): ClientConnection { + const clientConnection = new ClientConnection( + incoming, + request, + this, + this.hooks.bind(this), + { + timeout: this.configuration.timeout, + maxUnauthenticatedQueueSize: + this.configuration.maxUnauthenticatedQueueSize, + maxUnauthenticatedQueueMessages: + this.configuration.maxUnauthenticatedQueueMessages, + maxPendingDocuments: this.configuration.maxPendingDocuments, + }, + defaultContext, + ); + clientConnection.onClose( + (document: Document, hookPayload: onDisconnectPayload) => { + // Check if there are still no connections to the document, as these hooks + // may take some time to resolve (e.g. database queries). If a + // new connection were to come in during that time it would rely on the + // document in the map that we remove now. + if (document.getConnectionsCount() > 0) { + return; + } + + // If it’s the last connection, we need to make sure to store the + // document. Use the debouncer executeNow helper, to run scheduled + // onStoreDocument immediately and clear running timers. + // If there is no scheduled run for this document there is no point in + // triggering onStoreDocument hook, as everything seems to be stored already. + // Only run this if the document has finished loading earlier (i.e. not to persist the empty + // ydoc if the onLoadDocument hook returned an error) + if ( + !document.isLoading && + this.debouncer.isDebounced(`onStoreDocument-${document.name}`) + ) { + if (this.configuration.unloadImmediately) { + this.debouncer.executeNow(`onStoreDocument-${document.name}`); + } + } else { + // Remove document from memory immediately + this.unloadDocument(document); + } + }, + ); + + return clientConnection; + } + + /** + * Handle update of the given document + * + * "connection" is not necessarily type "Connection", it's the Yjs "origin" (which is "Connection" if + * the update is incoming from the provider, but can be anything if the updates is originated from an extension. + */ + private handleDocumentUpdate( + document: Document, + origin: unknown, + update: Uint8Array, + ) { + const connection = + isTransactionOrigin(origin) && origin.source === "connection" + ? origin.connection + : undefined; + const request = connection?.request; + const context = isTransactionOrigin(origin) + ? origin.source === "connection" + ? origin.connection.context + : origin.source === "local" + ? (origin.context ?? {}) + : {} + : {}; + + const changePayload: onChangePayload = { + instance: this, + clientsCount: document.getConnectionsCount(), + document, + documentName: document.name, + requestHeaders: request?.headers ?? new Headers(), + requestParameters: getParameters(request), + socketId: connection?.socketId ?? "", + update, + transactionOrigin: origin, + connection: connection, + context, + }; + + this.hooks("onChange", changePayload); + + if (shouldSkipStoreHooks(origin)) { + return; + } + + const storePayload: onStoreDocumentPayload = { + instance: this, + clientsCount: document.getConnectionsCount(), + document, + lastContext: context, + lastTransactionOrigin: origin, + documentName: document.name, + }; + + this.storeDocumentHooks(document, storePayload); + } + + /** + * Create a new document by the given request + */ + public async createDocument( + documentName: string, + request: Request, + socketId: string, + connection: ConnectionConfiguration, + context?: Context, + ): Promise { + if (!documentName.trim()) { + throw new Error("Document name must not be empty"); + } + + const existingLoadingDoc = this.loadingDocuments.get(documentName); + + if (existingLoadingDoc) { + return existingLoadingDoc; + } + + const existingDoc = this.documents.get(documentName); + if (existingDoc) { + return Promise.resolve(existingDoc); + } + + const loadDocPromise = this.loadDocument( + documentName, + request, + socketId, + connection, + context, + ); + + this.loadingDocuments.set(documentName, loadDocPromise); + + try { + const doc = await loadDocPromise; + this.documents.set(documentName, doc); + this.loadingDocuments.delete(documentName); + return doc; + } catch (e) { + this.loadingDocuments.delete(documentName); + throw e; + } + } + + async loadDocument( + documentName: string, + request: Request, + socketId: string, + connectionConfig: ConnectionConfiguration, + context?: Context, + ): Promise { + const requestHeaders = request.headers; + const requestParameters = getParameters(request); + + const resolvedContext = (context ?? {}) as Context; + + const yDocOptions = await this.hooks("onCreateDocument", { + documentName, + requestHeaders, + requestParameters, + connectionConfig, + context: resolvedContext, + socketId, + instance: this, + }); + + const document = new Document( + documentName, + { + ...this.configuration.yDocOptions, + ...yDocOptions, + }, + { + flushDelay: this.configuration.flushDelay, + flushMaxBytes: this.configuration.flushMaxBytes, + }, + ); + + const hookPayload = { + instance: this, + context: resolvedContext, + connectionConfig, + document, + documentName, + socketId, + requestHeaders, + requestParameters, + }; + + try { + await this.hooks( + "onLoadDocument", + hookPayload, + (loadedDocument: Doc | Uint8Array | undefined) => { + // the hook returned the document it was given, and encoding a + // document's own state to apply it back can never add content + if (loadedDocument === document) return; + + if (loadedDocument instanceof Doc) { + applyUpdate(document, encodeStateAsUpdate(loadedDocument)); + } else if (loadedDocument instanceof Uint8Array) { + applyUpdate(document, loadedDocument); + } + }, + ); + } catch (e) { + this.closeConnections(documentName); + // the document is not in `this.documents` yet — `createDocument` only + // registers it once this promise resolves — so `unloadDocument` would + // bail out before destroying it + document.destroy(); + throw e; + } + + document.isLoading = false; + + document.onUpdate( + (document: Document, origin: unknown, update: Uint8Array) => { + document.lastChangeTime = Date.now(); + + this.handleDocumentUpdate(document, origin, update); + }, + ); + + await this.hooks("afterLoadDocument", hookPayload); + + document.beforeBroadcastStateless( + (document: Document, stateless: string) => { + const hookPayload: beforeBroadcastStatelessPayload = { + document, + documentName: document.name, + payload: stateless, + }; + + this.hooks("beforeBroadcastStateless", hookPayload); + }, + ); + + document.beforeHandleAwareness((document, states, transactionOrigin) => { + const connection = + isTransactionOrigin(transactionOrigin) && + transactionOrigin.source === "connection" + ? transactionOrigin.connection + : undefined; + const request = connection?.request; + return this.hooks("beforeHandleAwareness", { + awareness: document.awareness, + clientsCount: document.getConnectionsCount(), + context: connection?.context, + document, + documentName: document.name, + instance: this, + requestHeaders: request?.headers ?? new Headers(), + requestParameters: request + ? getParameters(request) + : new URLSearchParams(), + socketId: connection?.socketId ?? "", + transactionOrigin, + connection, + states, + }); + }); + + document.awareness.on( + "update", + (update: AwarenessUpdate, origin: unknown) => { + this.hooks("onAwarenessUpdate", { + document, + documentName, + instance: this, + ...update, + transactionOrigin: origin, + connection: + isTransactionOrigin(origin) && origin.source === "connection" + ? origin.connection + : undefined, + awareness: document.awareness, + states: awarenessStatesToArray(document.awareness.getStates()), + }); + }, + ); + + return document; + } + + storeDocumentHooks( + document: Document, + hookPayload: onStoreDocumentPayload, + immediately?: boolean, + ) { + const debounceId = `onStoreDocument-${document.name}`; + return this.debouncer.debounce( + debounceId, + async () => { + try { + await document.saveMutex.runExclusive(async () => { + await this.hooks("onStoreDocument", hookPayload); + await this.hooks("afterStoreDocument", hookPayload); + }); + } catch (error: any) { + if (error instanceof SkipFurtherHooksError) { + // Another extension handled this — proceed to unload + setTimeout(() => { + if (this.shouldUnloadDocument(document)) { + this.unloadDocument(document); + } + }, 0); + return; + } + + console.error( + "Caught error during storeDocumentHooks. Document stays in memory to avoid data loss", + error, + ); + return; + } + + setTimeout(() => { + if (this.shouldUnloadDocument(document)) { + this.unloadDocument(document); + } + }, 0); + }, + immediately ? 0 : this.configuration.debounce, + this.configuration.maxDebounce, + ); + } + + /** + * Run the given hook on all configured extensions. + * Runs the given callback after each hook. + */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + hooks( + name: T, + payload: HookPayloadByName[T], + // biome-ignore lint/complexity/noBannedTypes: + callback: Function | null = null, + ): Promise { + const { extensions } = this.configuration; + + // create a new `thenable` chain + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + let chain = Promise.resolve(); + + extensions + // get me all extensions which have the given hook + .filter((extension) => typeof extension[name] === "function") + // run through all the configured hooks + .forEach((extension) => { + chain = chain + .then(() => (extension[name] as any)?.(payload)) + .catch((error) => { + // make sure to log error messages + if (error?.message) { + console.error(`[${name}]`, error.message); + } + + throw error; + }); + + if (callback) { + chain = chain.then((...args: any[]) => callback(...args)); + } + }); + + return chain; + } + + shouldUnloadDocument(document: Document): boolean { + const hasPendingWork = + this.debouncer.isDebounced(`onStoreDocument-${document.name}`) || + this.debouncer.isCurrentlyExecuting(`onStoreDocument-${document.name}`) || + document.saveMutex.isLocked(); + + return hasPendingWork === false && document.getConnectionsCount() === 0; + } + + async unloadDocument(document: Document): Promise { + const documentName = document.name; + + if (!this.shouldUnloadDocument(document)) return; + + if (!this.documents.has(documentName)) return; + + if (this.unloadingDocuments.has(documentName)) + return this.unloadingDocuments.get(documentName); + + // we need to make sure that the logic runs just once, even if multiple clients disconnect together + const actualUnloadingLogic = async () => { + try { + await this.hooks("beforeUnloadDocument", { + instance: this, + documentName, + document, + }); + } catch (e) { + return; + } + + // need sync check here as well, to avoid timing problems + if (!this.shouldUnloadDocument(document)) return; + + this.documents.delete(documentName); + document.destroy(); + await this.hooks("afterUnloadDocument", { instance: this, documentName }); + }; + + const unloading = actualUnloadingLogic(); + + this.unloadingDocuments.set(documentName, Promise.resolve(unloading)); + + await unloading; + + this.unloadingDocuments.delete(documentName); + } + + async openDirectConnection( + documentName: string, + context?: Context, + ): Promise> { + const connectionConfig: ConnectionConfiguration = { + isAuthenticated: true, + readOnly: false, + }; + + const document: Document = await this.createDocument( + documentName, + new Request("http://localhost"), // direct connection has no request params + crypto.randomUUID(), + connectionConfig, + context, + ); + + return new DirectConnection(document, this, context); + } +} diff --git a/packages/server/src/IncomingMessage.ts b/packages/server/src/IncomingMessage.ts new file mode 100644 index 0000000..04fa5a8 --- /dev/null +++ b/packages/server/src/IncomingMessage.ts @@ -0,0 +1,80 @@ +import type { Decoder } from "lib0/decoding"; +import { + createDecoder, + readVarString, + readVarUint, + readVarUint8Array, +} from "lib0/decoding"; +import type { Encoder } from "lib0/encoding"; +import { + createEncoder, + length, + toUint8Array, + writeVarString, + writeVarUint, +} from "lib0/encoding"; +import type { MessageType } from "./types.ts"; + +export class IncomingMessage { + /** + * Access to the received message. + */ + decoder: Decoder; + + /** + * Private encoder; can be undefined. + * + * Lazy creation of the encoder speeds up IncomingMessages that need only a decoder. + */ + private encoderInternal?: Encoder; + + constructor(input: any) { + if (!(input instanceof Uint8Array)) { + input = new Uint8Array(input); + } + + this.decoder = createDecoder(input); + } + + get encoder() { + if (!this.encoderInternal) { + this.encoderInternal = createEncoder(); + } + return this.encoderInternal; + } + + readVarUint8Array() { + return readVarUint8Array(this.decoder); + } + + peekVarUint8Array() { + const { pos } = this.decoder; + const result = readVarUint8Array(this.decoder); + this.decoder.pos = pos; + return result; + } + + readVarUint() { + return readVarUint(this.decoder); + } + + readVarString() { + return readVarString(this.decoder); + } + + toUint8Array() { + return toUint8Array(this.encoder); + } + + writeVarUint(type: MessageType) { + writeVarUint(this.encoder, type); + } + + writeVarString(string: string) { + writeVarString(this.encoder, string); + } + + get length(): number { + return length(this.encoder); + } +} diff --git a/packages/server/src/MessageReceiver.ts b/packages/server/src/MessageReceiver.ts new file mode 100644 index 0000000..d6b185f --- /dev/null +++ b/packages/server/src/MessageReceiver.ts @@ -0,0 +1,304 @@ +import { AuthMessageType } from "@hocuspocus/common"; +import * as decoding from "lib0/decoding"; +import { readVarString } from "lib0/decoding"; +import { + Awareness, + applyAwarenessUpdate, + encodeAwarenessUpdate, +} from "y-protocols/awareness"; +import { + messageYjsSyncStep1, + messageYjsSyncStep2, + messageYjsUpdate, + readSyncStep1, + readSyncStep2, + readUpdate, +} from "y-protocols/sync"; +import * as Y from "yjs"; +import type Connection from "./Connection.ts"; +import type Document from "./Document.ts"; +import type { IncomingMessage } from "./IncomingMessage.ts"; +import { OutgoingMessage } from "./OutgoingMessage.ts"; +import { + MessageType, + type ConnectionTransactionOrigin, + type TransactionOrigin, +} from "./types.ts"; + +export class MessageReceiver { + message: IncomingMessage; + + defaultTransactionOrigin?: TransactionOrigin; + + constructor( + message: IncomingMessage, + defaultTransactionOrigin?: TransactionOrigin, + ) { + this.message = message; + this.defaultTransactionOrigin = defaultTransactionOrigin; + } + + public async apply( + document: Document, + connection?: Connection, + reply?: (message: Uint8Array) => void, + ) { + const { message } = this; + const type = message.readVarUint(); + const emptyMessageLength = message.length; + + switch (type) { + case MessageType.Sync: + case MessageType.SyncReply: { + message.writeVarUint(MessageType.Sync); + await this.readSyncMessage( + message, + document, + connection, + reply, + type !== MessageType.SyncReply, + ); + + if (message.length > emptyMessageLength + 1) { + if (reply) { + reply(message.toUint8Array()); + } else if (connection) { + connection.send(message.toUint8Array()); + } + } + + break; + } + case MessageType.Awareness: { + let update = message.readVarUint8Array(); + + const origin: TransactionOrigin = connection + ? ({ + source: "connection", + connection, + } satisfies ConnectionTransactionOrigin) + : (this.defaultTransactionOrigin ?? { source: "local" }); + + // Decode the inbound update into a scratch Awareness so the hook + // chain sees a high-level Map. Mutations to that + // map (including `set`, `delete`, and field changes on each state + // object) are picked up by the re-encode below and forwarded as + // the broadcast payload. Hooks may also throw to reject the + // update entirely. + const scratchDoc = new Y.Doc(); + const scratch = new Awareness(scratchDoc); + try { + applyAwarenessUpdate(scratch, update, null); + + await document.callbacks.beforeHandleAwareness( + document, + scratch.getStates(), + origin, + ); + + update = encodeAwarenessUpdate(scratch, [ + ...scratch.getStates().keys(), + ]); + } finally { + scratch.destroy(); + scratchDoc.destroy(); + } + + applyAwarenessUpdate(document.awareness, update, origin); + + break; + } + case MessageType.QueryAwareness: { + this.applyQueryAwarenessMessage(document, connection, reply); + + break; + } + case MessageType.Stateless: { + connection?.callbacks.statelessCallback({ + connection, + documentName: document.name, + document, + payload: readVarString(message.decoder), + }); + + break; + } + case MessageType.BroadcastStateless: { + // Server-internal opcode used by @hocuspocus/extension-redis to + // fan a stateless payload across server instances. The Redis path + // invokes MessageReceiver without a `connection`, so a defined + // `connection` here means this frame came from a WebSocket client + // — which is never legitimate. Clients must use MessageType.Stateless + // (opcode 5); the onStateless hook is the authorization point and + // may call Document.broadcastStateless() to fan out if appropriate. + if (connection) { + throw new Error( + "BroadcastStateless is a server-internal opcode and cannot be sent from a client", + ); + } + const msg = message.readVarString(); + // `relayStateless`, not `broadcastStateless`: the originating + // instance already ran the beforeBroadcastStateless hook. It shares + // one encoded buffer across connections instead of encoding per + // connection, which matters here because this path fans out on + // every instance of a cluster. + document.relayStateless(msg); + break; + } + + case MessageType.CLOSE: { + connection?.close({ + code: 1000, + reason: "provider_initiated", + }); + break; + } + + case MessageType.Auth: { + const authType = message.readVarUint(); + if (authType === AuthMessageType.Token) { + connection?.callbacks.onTokenSyncCallback({ + token: message.readVarString(), + }); + break; + } + console.error( + "Received an authentication message on a connection that is already fully authenticated. Probably your provider has been destroyed + recreated really fast.", + ); + break; + } + + default: + console.error( + `Unable to handle message of type ${type}: no handler defined! Are your provider/server versions aligned?`, + ); + // Do nothing + } + } + + async readSyncMessage( + message: IncomingMessage, + document: Document, + connection?: Connection, + reply?: (message: Uint8Array) => void, + requestFirstSync = true, + ) { + const type = message.readVarUint(); + const messageAddress = connection?.messageAddress ?? document.name; + + if (connection) { + await connection.callbacks.beforeSync(connection, { + type, + payload: message.peekVarUint8Array(), + }); + } + + switch (type) { + case messageYjsSyncStep1: { + readSyncStep1(message.decoder, message.encoder, document); + + // When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. + if (reply && requestFirstSync) { + const syncMessage = new OutgoingMessage(messageAddress) + .createSyncReplyMessage() + .writeFirstSyncStepFor(document); + + reply(syncMessage.toUint8Array()); + } else if (connection) { + const syncMessage = new OutgoingMessage(messageAddress) + .createSyncMessage() + .writeFirstSyncStepFor(document); + + connection.send(syncMessage.toUint8Array()); + } + break; + } + case messageYjsSyncStep2: { + if (connection?.readOnly) { + // We're in read-only mode, so we can't apply the update. + // Let's use snapshotContainsUpdate to see if the update actually contains changes. + // If not, we can still ack the update + const snapshot = Y.snapshot(document); + const update = decoding.readVarUint8Array(message.decoder); + if (Y.snapshotContainsUpdate(snapshot, update)) { + // no new changes in update + const ackMessage = new OutgoingMessage( + messageAddress, + ).writeSyncStatus(true); + + connection.send(ackMessage.toUint8Array()); + } else { + // new changes in update that we can't apply, because readOnly + const ackMessage = new OutgoingMessage( + messageAddress, + ).writeSyncStatus(false); + + connection.send(ackMessage.toUint8Array()); + } + break; + } + + readSyncStep2( + message.decoder, + document, + connection + ? { source: "connection" as const, connection } + : (this.defaultTransactionOrigin ?? { source: "local" as const }), + ); + + if (connection) { + connection.send( + new OutgoingMessage(messageAddress) + .writeSyncStatus(true) + .toUint8Array(), + ); + } + break; + } + case messageYjsUpdate: { + if (connection?.readOnly) { + connection.send( + new OutgoingMessage(messageAddress) + .writeSyncStatus(false) + .toUint8Array(), + ); + break; + } + + readUpdate( + message.decoder, + document, + connection + ? { source: "connection" as const, connection } + : (this.defaultTransactionOrigin ?? { source: "local" as const }), + ); + if (connection) { + connection.send( + new OutgoingMessage(messageAddress) + .writeSyncStatus(true) + .toUint8Array(), + ); + } + break; + } + default: + throw new Error(`Received a message with an unknown type: ${type}`); + } + + return type; + } + + applyQueryAwarenessMessage( + document: Document, + connection?: Connection, + reply?: (message: Uint8Array) => void, + ) { + const message = new OutgoingMessage( + connection?.messageAddress ?? document.name, + ).createAwarenessUpdateMessage(document.awareness); + + if (reply) { + reply(message.toUint8Array()); + } + } +} diff --git a/packages/server/src/OutgoingMessage.ts b/packages/server/src/OutgoingMessage.ts new file mode 100644 index 0000000..fc6e3ae --- /dev/null +++ b/packages/server/src/OutgoingMessage.ts @@ -0,0 +1,159 @@ +import type { Encoder } from "lib0/encoding"; +import { + createEncoder, + toUint8Array, + writeVarString, + writeVarUint, + writeVarUint8Array, +} from "lib0/encoding"; +import type { Awareness } from "y-protocols/awareness"; +import { encodeAwarenessUpdate } from "y-protocols/awareness"; +import { writeSyncStep1, writeUpdate } from "y-protocols/sync"; + +import { writeAuthenticated, writePermissionDenied, writeTokenSyncRequest } from "@hocuspocus/common"; +import type Document from "./Document.ts"; +import { MessageType } from "./types.ts"; + +export class OutgoingMessage { + encoder: Encoder; + + type?: number; + + category?: string; + + constructor(documentName: string) { + this.encoder = createEncoder(); + + writeVarString(this.encoder, documentName); + } + + createSyncMessage(): OutgoingMessage { + this.type = MessageType.Sync; + + writeVarUint(this.encoder, MessageType.Sync); + + return this; + } + + createSyncReplyMessage(): OutgoingMessage { + this.type = MessageType.SyncReply; + + writeVarUint(this.encoder, MessageType.SyncReply); + + return this; + } + + createAwarenessUpdateMessage( + awareness: Awareness, + changedClients?: Array, + ): OutgoingMessage { + this.type = MessageType.Awareness; + this.category = "Update"; + + const message = encodeAwarenessUpdate( + awareness, + changedClients || Array.from(awareness.getStates().keys()), + ); + + writeVarUint(this.encoder, MessageType.Awareness); + writeVarUint8Array(this.encoder, message); + + return this; + } + + writeQueryAwareness(): OutgoingMessage { + this.type = MessageType.QueryAwareness; + this.category = "Update"; + + writeVarUint(this.encoder, MessageType.QueryAwareness); + + return this; + } + + writeTokenSyncRequest(): OutgoingMessage { + this.type = MessageType.Auth; + this.category = "TokenSync"; + + writeVarUint(this.encoder, MessageType.Auth); + writeTokenSyncRequest(this.encoder); + + return this; + } + + writeAuthenticated(readonly: boolean): OutgoingMessage { + this.type = MessageType.Auth; + this.category = "Authenticated"; + + writeVarUint(this.encoder, MessageType.Auth); + writeAuthenticated(this.encoder, readonly ? "readonly" : "read-write"); + + return this; + } + + writePermissionDenied(reason: string): OutgoingMessage { + this.type = MessageType.Auth; + this.category = "PermissionDenied"; + + writeVarUint(this.encoder, MessageType.Auth); + writePermissionDenied(this.encoder, reason); + + return this; + } + + writeFirstSyncStepFor(document: Document): OutgoingMessage { + this.category = "SyncStep1"; + + writeSyncStep1(this.encoder, document); + + return this; + } + + writeUpdate(update: Uint8Array): OutgoingMessage { + this.category = "Update"; + + writeUpdate(this.encoder, update); + + return this; + } + + writeStateless(payload: string): OutgoingMessage { + this.category = "Stateless"; + + writeVarUint(this.encoder, MessageType.Stateless); + writeVarString(this.encoder, payload); + + return this; + } + + writeBroadcastStateless(payload: string): OutgoingMessage { + this.category = "Stateless"; + + writeVarUint(this.encoder, MessageType.BroadcastStateless); + writeVarString(this.encoder, payload); + + return this; + } + + // TODO: should this be write* or create* as method name? + writeSyncStatus(updateSaved: boolean): OutgoingMessage { + this.category = "SyncStatus"; + + writeVarUint(this.encoder, MessageType.SyncStatus); + writeVarUint(this.encoder, updateSaved ? 1 : 0); + + return this; + } + + writeCloseMessage(reason: string): OutgoingMessage { + this.type = MessageType.CLOSE; + + writeVarUint(this.encoder, MessageType.CLOSE); + writeVarString(this.encoder, reason); + + return this; + } + + toUint8Array(): Uint8Array { + return toUint8Array(this.encoder); + } +} diff --git a/packages/server/src/Server.ts b/packages/server/src/Server.ts new file mode 100644 index 0000000..ce2d402 --- /dev/null +++ b/packages/server/src/Server.ts @@ -0,0 +1,285 @@ +import type { + Server as HTTPServer, + IncomingMessage, + ServerResponse, +} from "node:http"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import type { ListenOptions } from "node:net"; +import crossws from "crossws/adapters/node"; +import kleur from "kleur"; +import meta from "../package.json" assert { type: "json" }; +import { Hocuspocus, defaultConfiguration } from "./Hocuspocus.ts"; +import type { Configuration, WebSocketLike, onListenPayload } from "./types.ts"; + +export interface ServerConfiguration + extends Configuration { + port?: number; + address?: string; + stopOnSignals?: boolean; + /** + * Options passed to the underlying WebSocket server (ws). + * Supports all ws ServerOptions, e.g. { maxPayload: 1024 * 1024 } + */ + websocketOptions?: Record; +} + +export const defaultServerConfiguration = { + port: 80, + stopOnSignals: true, +}; + +export class Server { + httpServer: HTTPServer; + + private crossws: ReturnType; + + hocuspocus: Hocuspocus; + + configuration: ServerConfiguration = { + ...defaultConfiguration, + ...defaultServerConfiguration, + extensions: [], + }; + + constructor(configuration?: Partial>) { + if (configuration) { + this.configuration = { + ...this.configuration, + ...configuration, + }; + } + + this.hocuspocus = new Hocuspocus(this.configuration); + this.hocuspocus.server = this; + + this.httpServer = createServer(this.requestHandler); + this.crossws = crossws({ + serverOptions: this.configuration.websocketOptions, + hooks: { + open: (peer) => { + const clientConnection = this.hocuspocus.handleConnection( + peer.websocket as unknown as WebSocketLike, + peer.request as Request, + ); + (peer as any)._hocuspocus = clientConnection; + }, + message: (peer, message) => { + (peer as any)._hocuspocus?.handleMessage(message.uint8Array()); + }, + close: (peer, event) => { + (peer as any)._hocuspocus?.handleClose({ + code: event.code, + reason: event.reason, + }); + }, + error: (peer, error) => { + console.error("WebSocket error for peer:", peer.id); + console.error(error); + }, + }, + }); + + this.setupHttpUpgrade(); + } + + private setupHttpUpgrade = () => { + this.httpServer.on("upgrade", async (request, socket, head) => { + try { + await this.hocuspocus.hooks("onUpgrade", { + request, + socket, + head, + instance: this.hocuspocus, + }); + + // Let crossws handle the WebSocket upgrade + this.crossws.handleUpgrade(request, socket, head); + } catch (error) { + // if a hook rejects and the error is empty, do nothing + // this is only meant to prevent later hooks and the + // default handler to do something. if a error is present + // just rethrow it + if (error) { + throw error; + } + } + }); + }; + + requestHandler = async ( + request: IncomingMessage, + response: ServerResponse, + ) => { + try { + await this.hocuspocus.hooks("onRequest", { + request, + response, + instance: this.hocuspocus, + }); + + // default response if all prior hooks don't interfere + response.writeHead(200, { "Content-Type": "text/plain" }); + response.end("Welcome to Hocuspocus!"); + } catch (error) { + // if a hook rejects and the error is empty, do nothing + // this is only meant to prevent later hooks and the + // default handler to do something. if a error is present + // just rethrow it + if (error) { + throw error; + } + } + }; + + async listen( + port?: number, + callback: any = null, + ): Promise> { + if (port) { + this.configuration.port = port; + } + + if (typeof callback === "function") { + this.hocuspocus.configuration.extensions.push({ + onListen: callback, + }); + } + + if (this.configuration.stopOnSignals) { + const signalHandler = async () => { + await this.destroy(); + process.exit(0); + }; + + process.on("SIGINT", signalHandler); + process.on("SIGQUIT", signalHandler); + process.on("SIGTERM", signalHandler); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + return new Promise((resolve: Function, reject: Function) => { + const listenOptions: ListenOptions = { + port: this.configuration.port, + host: this.configuration.address, + }; + + this.httpServer.listen( + listenOptions, + async () => { + if ( + !this.configuration.quiet && + String(process.env.NODE_ENV) !== "testing" + ) { + this.showStartScreen(); + } + + const onListenPayload = { + instance: this.hocuspocus, + configuration: this.configuration, + port: this.address.port, + } as onListenPayload; + + try { + await this.hocuspocus.hooks("onListen", onListenPayload); + resolve(this.hocuspocus); + } catch (e) { + reject(e); + } + }, + ); + }); + } + + get address(): AddressInfo { + return (this.httpServer.address() || { + port: this.configuration.port, + address: this.configuration.address ?? "0.0.0.0", + family: "IPv4", + }) as AddressInfo; + } + + private destroyPromise?: Promise; + + async destroy(): Promise { + // Guard against multiple shutdowns (e.g. repeated SIGINT signals). + // Subsequent callers await the same in-flight shutdown rather than + // re-running close hooks on already-closed resources. + this.destroyPromise ??= this.runDestroy(); + + return this.destroyPromise; + } + + private async runDestroy(): Promise { + await new Promise((resolve) => { + this.httpServer.close(); + + try { + this.configuration.extensions.push({ + async afterUnloadDocument({ instance }) { + if (instance.getDocumentsCount() === 0) resolve(); + }, + }); + + // Close all existing connections - this will trigger the close hook + if (this.hocuspocus.getDocumentsCount() === 0) resolve(); + + this.hocuspocus.closeConnections(); + + // Flush any remaining debounced stores so documents unload + // promptly, even when unloadImmediately is false. + this.hocuspocus.flushPendingStores(); + } catch (error) { + console.error(error); + } + }); + + await this.hocuspocus.hooks("onDestroy", { instance: this.hocuspocus }); + } + + get URL(): string { + return `${this.configuration.address ?? "0.0.0.0"}:${this.address.port}`; + } + + get webSocketURL(): string { + return `ws://${this.URL}`; + } + + get httpURL(): string { + return `http://${this.URL}`; + } + + private showStartScreen() { + const name = this.configuration.name ? ` (${this.configuration.name})` : ""; + + console.log(); + console.log( + ` ${kleur.cyan(`Hocuspocus v${meta.version}${name}`)}${kleur.green(" running at:")}`, + ); + console.log(); + + console.log(` > HTTP: ${kleur.cyan(`${this.httpURL}`)}`); + console.log(` > WebSocket: ${this.webSocketURL}`); + + const extensions = this.configuration?.extensions + .map((extension) => { + return extension.extensionName ?? extension.constructor?.name; + }) + .filter((name) => name) + .filter((name) => name !== "Object"); + + if (!extensions.length) { + return; + } + + console.log(); + console.log(" Extensions:"); + + extensions.forEach((name) => { + console.log(` - ${name}`); + }); + + console.log(); + console.log(` ${kleur.green("Ready.")}`); + console.log(); + } +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..8ede6d1 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,9 @@ +export * from "./Connection.ts"; +export * from "./Document.ts"; +export * from "./Hocuspocus.ts"; +export * from "./IncomingMessage.ts"; +export * from "./MessageReceiver.ts"; +export * from "./OutgoingMessage.ts"; +export * from "./Server.ts"; +export * from "./types.ts"; +export * from "./util/debounce.ts"; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts new file mode 100644 index 0000000..3c4b98c --- /dev/null +++ b/packages/server/src/types.ts @@ -0,0 +1,619 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import type { Awareness } from "y-protocols/awareness"; +import type Connection from "./Connection.ts"; +import type Document from "./Document.ts"; +import type { Hocuspocus } from "./Hocuspocus.ts"; + +export interface ConnectionTransactionOrigin { + source: "connection"; + connection: Connection; +} + +export interface RedisTransactionOrigin { + source: "redis"; +} + +export interface LocalTransactionOrigin { + source: "local"; + skipStoreHooks?: boolean; + context?: any; +} + +export type TransactionOrigin = + | ConnectionTransactionOrigin + | RedisTransactionOrigin + | LocalTransactionOrigin; + +export function isTransactionOrigin( + origin: unknown, +): origin is TransactionOrigin { + return ( + typeof origin === "object" && + origin !== null && + "source" in origin && + ((origin as any).source === "connection" || + (origin as any).source === "redis" || + (origin as any).source === "local") + ); +} + +export function shouldSkipStoreHooks(origin: unknown): boolean { + if (!isTransactionOrigin(origin)) return false; + switch (origin.source) { + case "connection": + return false; + case "redis": + return true; + case "local": + return origin.skipStoreHooks ?? false; + } +} + +/** + * Minimal interface for any WebSocket-like object for WebSocket, Bun's ServerWebSocket, ws, Deno, etc. + */ +export interface WebSocketLike { + send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; + close(code?: number, reason?: string): void; + readyState: number; +} + +export enum MessageType { + Unknown = -1, + Sync = 0, + Awareness = 1, + Auth = 2, + QueryAwareness = 3, + SyncReply = 4, // same as Sync, but won't trigger another 'SyncStep1' + Stateless = 5, + BroadcastStateless = 6, + CLOSE = 7, + SyncStatus = 8, + Ping = 9, + Pong = 10, +} + +export interface AwarenessUpdate { + added: Array; + updated: Array; + removed: Array; +} + +export interface ConnectionConfiguration { + readOnly: boolean; + isAuthenticated: boolean; +} + +export interface Extension { + priority?: number; + extensionName?: string; + onConfigure?(data: onConfigurePayload): Promise; + onListen?(data: onListenPayload): Promise; + onUpgrade?(data: onUpgradePayload): Promise; + onConnect?(data: onConnectPayload): Promise; + connected?(data: connectedPayload): Promise; + onAuthenticate?(data: onAuthenticatePayload): Promise; + onTokenSync?(data: onTokenSyncPayload): Promise; + onCreateDocument?(data: onCreateDocumentPayload): Promise; + onLoadDocument?(data: onLoadDocumentPayload): Promise; + afterLoadDocument?(data: afterLoadDocumentPayload): Promise; + beforeHandleMessage?(data: beforeHandleMessagePayload): Promise; + /** + * Fired after an inbound message has been handled (i.e. after the Yjs + * update has been applied to the document). Fires whether the apply + * resolved or threw, but not when `beforeHandleMessage` rejected the + * message. + */ + afterHandleMessage?(data: afterHandleMessagePayload): Promise; + /** + * Fired before an inbound awareness update is applied to the document's + * awareness state. The hook receives the decoded per-client `states` as a + * mutable `Map` keyed by Yjs clientId. Mutate the map and the contained + * state objects in place to rewrite fields, drop peers (`states.delete`), + * or add synthetic ones (`states.set`); mutations are reflected in the + * broadcast. Throw to reject the update without applying anything. + * + * Multiple extensions chain naturally: each extension sees the map as + * mutated by previous extensions and can mutate it further. + */ + beforeHandleAwareness?( + data: beforeHandleAwarenessPayload, + ): Promise; + beforeSync?(data: beforeSyncPayload): Promise; + beforeBroadcastStateless?( + data: beforeBroadcastStatelessPayload, + ): Promise; + onStateless?(payload: onStatelessPayload): Promise; + onChange?(data: onChangePayload): Promise; + onStoreDocument?(data: onStoreDocumentPayload): Promise; + afterStoreDocument?(data: afterStoreDocumentPayload): Promise; + onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise; + onRequest?(data: onRequestPayload): Promise; + onDisconnect?(data: onDisconnectPayload): Promise; + beforeUnloadDocument?(data: beforeUnloadDocumentPayload): Promise; + afterUnloadDocument?(data: afterUnloadDocumentPayload): Promise; + onDestroy?(data: onDestroyPayload): Promise; +} + +export type HookName = + | "onConfigure" + | "onListen" + | "onUpgrade" + | "onConnect" + | "connected" + | "onAuthenticate" + | "onTokenSync" + | "onCreateDocument" + | "onLoadDocument" + | "afterLoadDocument" + | "beforeHandleMessage" + | "afterHandleMessage" + | "beforeHandleAwareness" + | "beforeBroadcastStateless" + | "beforeSync" + | "onStateless" + | "onChange" + | "onStoreDocument" + | "afterStoreDocument" + | "onAwarenessUpdate" + | "onRequest" + | "onDisconnect" + | "beforeUnloadDocument" + | "afterUnloadDocument" + | "onDestroy"; + +export type HookPayloadByName = { + onConfigure: onConfigurePayload; + onListen: onListenPayload; + onUpgrade: onUpgradePayload; + onConnect: onConnectPayload; + connected: connectedPayload; + onAuthenticate: onAuthenticatePayload; + onTokenSync: onTokenSyncPayload; + onCreateDocument: onCreateDocumentPayload; + onLoadDocument: onLoadDocumentPayload; + afterLoadDocument: afterLoadDocumentPayload; + beforeHandleMessage: beforeHandleMessagePayload; + afterHandleMessage: afterHandleMessagePayload; + beforeHandleAwareness: beforeHandleAwarenessPayload; + beforeBroadcastStateless: beforeBroadcastStatelessPayload; + beforeSync: beforeSyncPayload; + onStateless: onStatelessPayload; + onChange: onChangePayload; + onStoreDocument: onStoreDocumentPayload; + afterStoreDocument: afterStoreDocumentPayload; + onAwarenessUpdate: onAwarenessUpdatePayload; + onRequest: onRequestPayload; + onDisconnect: onDisconnectPayload; + afterUnloadDocument: afterUnloadDocumentPayload; + beforeUnloadDocument: beforeUnloadDocumentPayload; + onDestroy: onDestroyPayload; +}; + +/** + * Controls how a document batches its outgoing broadcasts. Shared by the server + * configuration and the `Document` constructor so both stay in step. + */ +export interface FlushOptions { + /** + * Batch outgoing broadcasts per document over a window of this many + * milliseconds, instead of sending one message per connection per change. + * Updates within a window are merged into a single message and awareness + * collapses to the latest state of each changed client, so a document with + * many connected clients sends `connections` messages per window rather than + * `changes × connections`. + * + * The default `0` flushes at the end of the current event loop turn. It adds + * no delay — a burst of updates that arrived together is coalesced before + * anything is written to a socket — and with a single writer no merge happens + * at all, because there is only one buffered update to send. On a document + * with many writing clients the difference is large: 2000 clients each + * writing once produces 2000 broadcast messages per turn instead of + * 2000 × 2000. + * + * A positive value additionally merges across turns, trading up to that much + * latency for fewer messages. That is only a win when several changes + * genuinely land per window — with a handful of clients typing it is pure + * added delay, so prefer `0` unless you have measured otherwise. + * + * `false` restores the pre-batching behaviour and sends every broadcast + * synchronously, inside the same tick that applied the change. + * + * Applies to the WebSocket fan-out only. `onChange`, the `onStoreDocument` + * debounce and the Redis publish still run synchronously per change. + * + * @default 0 (coalesce within the current event loop turn) + */ + flushDelay: false | number; + + /** + * Flush a document's batched broadcasts early once this many bytes of updates + * are buffered, bounding both the memory held per window and the cost of the + * merge. Ignored when `flushDelay` is false. + * + * @default 1_048_576 (1 MiB) + */ + flushMaxBytes: number; +} + +export interface Configuration + extends Extension, + FlushOptions { + /** + * A name for the instance, used for logging. + */ + name: string | null; + /** + * A list of hocuspocus extensions. + */ + extensions: Array; + /** + * Defines in which interval the server sends a ping, and closes the connection when no pong is sent back. + */ + timeout: number; + /** + * Debounces the call of the `onStoreDocument` hook for the given amount of time in ms. + * Otherwise every single update would be persisted. + */ + debounce: number; + /** + * Makes sure to call `onStoreDocument` at least in the given amount of time (ms). + */ + maxDebounce: number; + /** + * By default, the servers show a start screen. If passed false, the server will start quietly. + */ + quiet: boolean; + /** + * If set to false, respects the debounce time of `onStoreDocument` before unloading a document. + * Otherwise, the document will be unloaded immediately. + * + * This prevents a client from DOSing the server by repeatedly connecting and disconnecting when + * your onStoreDocument is rate-limited. + */ + unloadImmediately: boolean; + + /** + * Maximum number of bytes that may be buffered, across all documents, for a single + * connection while it is still unauthenticated. Messages received before authentication + * completes are queued in memory; without a limit an unauthenticated client could exhaust + * server memory (see GHSA-xwhh-v746-pj9m). When the limit is exceeded the connection is closed. + * + * @default 5_242_880 (5 MiB) + */ + maxUnauthenticatedQueueSize: number; + /** + * Maximum number of messages that may be buffered, across all documents, for a single + * connection while it is still unauthenticated. When the limit is exceeded the connection + * is closed. + * + * @default 1_000 + */ + maxUnauthenticatedQueueMessages: number; + /** + * Maximum number of distinct documents a single connection may open before any of them is + * authenticated. Each pending document holds its own queue and hook payload, so an + * unauthenticated client fanning out across many document names could otherwise exhaust + * memory even while staying under the per-connection queue limits. When the limit is + * exceeded the connection is closed. + * + * @default 100 + */ + maxPendingDocuments: number; + + /** + * options to pass to the ydoc document + */ + yDocOptions: { + gc: boolean; // enable or disable garbage collection (see https://github.com/yjs/yjs/blob/main/INTERNALS.md#deletions) + gcFilter: () => boolean; // will be called before garbage collecting ; return false to keep it + }; +} + +export interface onStatelessPayload { + connection: Connection; + documentName: string; + document: Document; + payload: string; +} + +export interface onAuthenticatePayload { + context: Context; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + request: Request; + socketId: string; + token: string; + connectionConfig: ConnectionConfiguration; + providerVersion: string | null; +} + +export interface onTokenSyncPayload { + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + token: string; + connectionConfig: ConnectionConfiguration; + connection: Connection; +} + +export interface onCreateDocumentPayload { + context: Context; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; +} + +export interface onConnectPayload { + context: Context; + documentName: string; + instance: Hocuspocus; + request: Request; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; + providerVersion: string | null; +} + +export interface connectedPayload { + context: Context; + documentName: string; + instance: Hocuspocus; + request: Request; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; + connection: Connection; + providerVersion: string | null; +} + +export interface onLoadDocumentPayload { + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; +} + +export interface afterLoadDocumentPayload { + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; +} + +export interface onChangePayload { + clientsCount: number; + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + update: Uint8Array; + socketId: string; + transactionOrigin: unknown; + connection?: Connection; +} + +export interface beforeHandleMessagePayload { + clientsCount: number; + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + update: Uint8Array; + socketId: string; + connection: Connection; +} + +export interface afterHandleMessagePayload { + clientsCount: number; + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + update: Uint8Array; + socketId: string; + connection: Connection; +} + +export interface beforeHandleAwarenessPayload { + awareness: Awareness; + clientsCount: number; + /** + * Connection context populated by `onAuthenticate`. `undefined` when the + * update did not originate from a client connection (e.g. server-internal + * writes via `DirectConnection`). + */ + context: Context | undefined; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + /** + * Per-client awareness states decoded from the inbound update, keyed by + * Yjs clientId. Mutate this map in place to rewrite the update: change + * fields on a state object, `states.delete(clientId)` to drop a peer, or + * `states.set(clientId, ...)` to add or replace one. The encoded update + * sent to peers reflects whatever the map looks like after every hook in + * the chain has run. + */ + states: Map>; + socketId: string; + /** + * The `TransactionOrigin` that will be passed to `applyAwarenessUpdate`. + * Use `isTransactionOrigin(origin)` to discriminate sources. Matches the + * `transactionOrigin` shape of `onAwarenessUpdatePayload`. + */ + transactionOrigin: unknown; + /** + * Convenience shortcut: `origin.connection` when `transactionOrigin` is a + * `ConnectionTransactionOrigin`, otherwise `undefined`. Matches the + * `connection?` shape of `onAwarenessUpdatePayload`. + */ + connection?: Connection; +} + +export interface beforeSyncPayload { + clientsCount: number; + context: Context; + document: Document; + documentName: string; + connection: Connection; + /** + * The y-protocols/sync message type + * @example + * 0: SyncStep1 + * 1: SyncStep2 + * 2: YjsUpdate + * + * @see https://github.com/yjs/y-protocols/blob/master/sync.js#L13-L40 + */ + type: number; + /** + * The payload of the y-sync message. + */ + payload: Uint8Array; +} + +export interface beforeBroadcastStatelessPayload { + document: Document; + documentName: string; + payload: string; +} + +export interface onStoreDocumentPayload { + clientsCount: number; + document: Document; + lastContext: Context; + lastTransactionOrigin: unknown; + documentName: string; + instance: Hocuspocus; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface afterStoreDocumentPayload + extends onStoreDocumentPayload {} + +export interface onAwarenessUpdatePayload { + document: Document; + documentName: string; + instance: Hocuspocus; + transactionOrigin: unknown; + connection?: Connection; + added: number[]; + updated: number[]; + removed: number[]; + awareness: Awareness; + states: StatesArray; +} + +export type StatesArray = { clientId: number; [key: string | number]: any }[]; + +export interface fetchPayload { + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; + connectionConfig: ConnectionConfiguration; +} + +export interface storePayload + extends onStoreDocumentPayload { + state: Buffer; +} + +export interface onDisconnectPayload { + clientsCount: number; + context: Context; + document: Document; + documentName: string; + instance: Hocuspocus; + requestHeaders: Headers; + requestParameters: URLSearchParams; + socketId: string; +} + +export interface onRequestPayload { + request: IncomingMessage; + response: ServerResponse; + instance: Hocuspocus; +} + +export interface onUpgradePayload { + request: IncomingMessage; + socket: any; + head: any; + instance: Hocuspocus; +} + +export interface onListenPayload { + instance: Hocuspocus; + configuration: Configuration; + port: number; +} + +export interface onDestroyPayload { + instance: Hocuspocus; +} + +export interface onConfigurePayload { + instance: Hocuspocus; + configuration: Configuration; + version: string; +} + +export interface afterUnloadDocumentPayload { + instance: Hocuspocus; + documentName: string; +} + +export interface beforeUnloadDocumentPayload { + instance: Hocuspocus; + documentName: string; + document: Document; +} + +export interface DisconnectOptions { + /** + * When `true` (the default) the document is persisted immediately (no debouncing) + * and unloaded from memory as the connection closes. When `false` the store is + * scheduled via the debounce timer and the document is kept warm in memory, so a + * follow-up direct connection reuses it and repeated writes coalesce. With + * `false`, persistence is no longer guaranteed by the time `disconnect()` + * resolves. + */ + unloadImmediately?: boolean; +} + +export interface DirectConnection { + transact(transaction: (document: Document) => void): Promise; + disconnect(options?: DisconnectOptions): Promise; +} diff --git a/packages/server/src/util/debounce.ts b/packages/server/src/util/debounce.ts new file mode 100644 index 0000000..19db7be --- /dev/null +++ b/packages/server/src/util/debounce.ts @@ -0,0 +1,77 @@ +export const useDebounce = () => { + const timers: Map< + string, + { + timeout: NodeJS.Timeout; + start: number; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + func: () => any | Promise<() => any>; + } + > = new Map(); + + const runningExecutions: Map> = new Map(); + + const debounce = async ( + id: string, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + func: () => any | Promise<() => any>, + debounce: number, + maxDebounce: number, + ) => { + const old = timers.get(id); + const start = old?.start || Date.now(); + + const run = async () => { + timers.delete(id); + + if (runningExecutions.has(id)) { + // wait for previous execution to finish + await runningExecutions.get(id); + } + + const execution = func(); + + runningExecutions.set(id, execution); + const executionResult = await execution; + runningExecutions.delete(id); + + return executionResult; + }; + + if (old?.timeout) { + clearTimeout(old.timeout); + } + + if (debounce === 0) { + return run(); + } + + if (Date.now() - start >= maxDebounce) { + return run(); + } + + timers.set(id, { + start, + timeout: setTimeout(run, debounce), + func: run, + }); + }; + + const executeNow = (id: string) => { + const old = timers.get(id); + if (old) { + clearTimeout(old.timeout); + return old.func(); + } + }; + + const isDebounced = (id: string): boolean => { + return timers.has(id); + }; + + const isCurrentlyExecuting = (id: string): boolean => { + return runningExecutions.has(id); + }; + + return { debounce, isDebounced, isCurrentlyExecuting, executeNow }; +}; diff --git a/packages/server/src/util/getParameters.ts b/packages/server/src/util/getParameters.ts new file mode 100644 index 0000000..906c4bf --- /dev/null +++ b/packages/server/src/util/getParameters.ts @@ -0,0 +1,14 @@ +/** + * Get parameters by the given request + */ +export function getParameters(request?: { url?: string }): URLSearchParams { + const url = request?.url; + if (!url) { + return new URLSearchParams(); + } + // Handle both full URLs (web Request) and path-only URLs (Node.js IncomingMessage) + const query = url.includes("://") + ? new URL(url).searchParams + : new URLSearchParams(url.split("?")[1] || ""); + return query; +} diff --git a/packages/transformer/CHANGELOG.md b/packages/transformer/CHANGELOG.md new file mode 100644 index 0000000..b36729b --- /dev/null +++ b/packages/transformer/CHANGELOG.md @@ -0,0 +1,1269 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.7.0](https://github.com/ueberdosis/hocuspocus/compare/v4.6.0...v4.7.0) (2026-09-09) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.6.0](https://github.com/ueberdosis/hocuspocus/compare/v4.5.0...v4.6.0) (2026-08-10) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.5.0](https://github.com/ueberdosis/hocuspocus/compare/v4.4.0...v4.5.0) (2026-08-04) + + +### Bug Fixes + +* update packages via audit --fix ([1dc9ca0](https://github.com/ueberdosis/hocuspocus/commit/1dc9ca0ff35f1033136473d134cee8cb6b336281)) + + + + + +# [4.4.0](https://github.com/ueberdosis/hocuspocus/compare/v4.3.0...v4.4.0) (2026-07-13) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.3.0](https://github.com/ueberdosis/hocuspocus/compare/v4.2.0...v4.3.0) (2026-06-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.2.0](https://github.com/ueberdosis/hocuspocus/compare/v4.1.2...v4.2.0) (2026-06-12) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [4.1.2](https://github.com/ueberdosis/hocuspocus/compare/v4.1.1...v4.1.2) (2026-06-11) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [4.1.1](https://github.com/ueberdosis/hocuspocus/compare/v4.1.0...v4.1.1) (2026-06-10) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.1.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0...v4.1.0) (2026-05-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.7...v4.0.0) (2026-04-23) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.7](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.6...v4.0.0-rc.7) (2026-04-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.6](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2026-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-04-16) + + +### Features + +* adds package readmes ([c7d63f7](https://github.com/ueberdosis/hocuspocus/commit/c7d63f79980de3c34e63b60073f22774bf27ac13)) + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-04-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-04-16) + + +### Features + +* first draft of @hocuspocus/provider-react package ([#1059](https://github.com/ueberdosis/hocuspocus/issues/1059)) ([cbc9977](https://github.com/ueberdosis/hocuspocus/commit/cbc99770096e8c674dae304c7e1a16e0fbec827b)) + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-04-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-30) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.5](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.4](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.3](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v4.0.0-rc.0...v4.0.0-rc.1) (2026-03-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [4.0.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v4.0.0-rc.0) (2026-02-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.6-rc.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.1...v3.4.6-rc.2) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.11](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.11) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.9](https://github.com/ueberdosis/hocuspocus/compare/v3.1.8...v3.1.9) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + + +### Bug Fixes + +* export typings for recent typescript ([#602](https://github.com/ueberdosis/hocuspocus/issues/602)) ([551d27f](https://github.com/ueberdosis/hocuspocus/commit/551d27fa6de9c746c67bf0f1e3bb56167aebbca5)), closes [/github.com/artalar/reatom/issues/560#issuecomment-1528997739](https://github.com//github.com/artalar/reatom/issues/560/issues/issuecomment-1528997739) + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + + +### Bug Fixes + +* make `transformer` compatible with `@tiptap/core@^2.0.0-beta.210` ([#512](https://github.com/ueberdosis/hocuspocus/issues/512)) ([591103f](https://github.com/ueberdosis/hocuspocus/commit/591103fe15d3c3760b7e59e65785a34e1d736c16)) + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Features + +* add default schema to ProsemirrorTransformer and default extensions to TiptapTransformer, making both parameters in toYdoc optional ([e330ffd](https://github.com/ueberdosis/hocuspocus/commit/e330ffd81f8cbd98c1d57545d3ec9d51807f7db3)) + + + + + +# [1.0.0-alpha.23](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.22...@hocuspocus/transformer@1.0.0-alpha.23) (2022-04-05) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.22](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.21...@hocuspocus/transformer@1.0.0-alpha.22) (2022-03-21) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.21](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.20...@hocuspocus/transformer@1.0.0-alpha.21) (2022-03-11) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.20](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.19...@hocuspocus/transformer@1.0.0-alpha.20) (2022-02-24) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.19](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.18...@hocuspocus/transformer@1.0.0-alpha.19) (2022-02-18) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.18](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.17...@hocuspocus/transformer@1.0.0-alpha.18) (2021-12-07) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.17](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.16...@hocuspocus/transformer@1.0.0-alpha.17) (2021-11-26) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.16](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.15...@hocuspocus/transformer@1.0.0-alpha.16) (2021-11-22) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.14...@hocuspocus/transformer@1.0.0-alpha.15) (2021-10-31) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.13...@hocuspocus/transformer@1.0.0-alpha.14) (2021-08-13) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.13](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.12...@hocuspocus/transformer@1.0.0-alpha.13) (2021-07-07) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.12](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.11...@hocuspocus/transformer@1.0.0-alpha.12) (2021-06-11) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.11](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.10...@hocuspocus/transformer@1.0.0-alpha.11) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.10](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.9...@hocuspocus/transformer@1.0.0-alpha.10) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.9](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.8...@hocuspocus/transformer@1.0.0-alpha.9) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.8](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.7...@hocuspocus/transformer@1.0.0-alpha.8) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.7](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.6...@hocuspocus/transformer@1.0.0-alpha.7) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.5...@hocuspocus/transformer@1.0.0-alpha.6) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.4...@hocuspocus/transformer@1.0.0-alpha.5) (2021-04-07) + + +### Features + +* add default schema to ProsemirrorTransformer and default extensions to TiptapTransformer, making both parameters in toYdoc optional ([e330ffd](https://github.com/ueberdosis/hocuspocus/commit/e330ffd81f8cbd98c1d57545d3ec9d51807f7db3)) + + + + + +# [1.0.0-alpha.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.3...@hocuspocus/transformer@1.0.0-alpha.4) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# [1.0.0-alpha.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/transformer@1.0.0-alpha.2...@hocuspocus/transformer@1.0.0-alpha.3) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer + + + + + +# 1.0.0-alpha.2 (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/transformer diff --git a/packages/transformer/README.md b/packages/transformer/README.md new file mode 100644 index 0000000..0afc5f1 --- /dev/null +++ b/packages/transformer/README.md @@ -0,0 +1,14 @@ +# @hocuspocus/transformer +[![Version](https://img.shields.io/npm/v/@hocuspocus/transformer.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/transformer) +[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/transformer.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@hocuspocus/transformer.svg)](https://www.npmjs.com/package/@hocuspocus/transformer) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Introduction +Convert [Y.js](https://github.com/yjs/yjs) documents to and from [Tiptap](https://github.com/ueberdosis/tiptap) / ProseMirror JSON and HTML. Useful on the server side to render or process collaborative document state outside the editor – for exports, previews, or webhook payloads. + +## Official Documentation +Documentation can be found in the [GitHub repository](https://github.com/ueberdosis/hocuspocus). + +## License +Hocuspocus is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md). diff --git a/packages/transformer/package.json b/packages/transformer/package.json new file mode 100644 index 0000000..ed67b56 --- /dev/null +++ b/packages/transformer/package.json @@ -0,0 +1,49 @@ +{ + "name": "@hocuspocus/transformer", + "version": "4.7.0", + "description": "hocuspocus transformation utilities", + "homepage": "https://hocuspocus.dev", + "keywords": [ + "hocuspocus", + "transformer", + "yjs" + ], + "license": "MIT", + "type": "module", + "main": "dist/hocuspocus-transformer.cjs", + "module": "dist/hocuspocus-transformer.esm.js", + "types": "dist/index.d.ts", + "exports": { + "source": { + "import": "./src/index.ts" + }, + "default": { + "import": "./dist/hocuspocus-transformer.esm.js", + "require": "./dist/hocuspocus-transformer.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "src", + "dist" + ], + "dependencies": { + "@tiptap/starter-kit": "^3.19.0" + }, + "peerDependencies": { + "@tiptap/core": "^3.0.1", + "@tiptap/pm": "^3.0.1", + "y-prosemirror": "^1.2.1", + "yjs": "^13.6.8" + }, + "devDependencies": { + "@tiptap/pm": "^3.19.0" + }, + "gitHead": "8f2e9df95de9968d70622ea8697c303a333b6cb3", + "repository": { + "url": "https://github.com/ueberdosis/hocuspocus" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/transformer/src/Prosemirror.ts b/packages/transformer/src/Prosemirror.ts new file mode 100644 index 0000000..2c21dfa --- /dev/null +++ b/packages/transformer/src/Prosemirror.ts @@ -0,0 +1,76 @@ +import { Doc, applyUpdate, encodeStateAsUpdate } from "yjs"; +// @ts-expect-error +import { yDocToProsemirrorJSON, prosemirrorJSONToYDoc } from "y-prosemirror"; +import { Schema } from "@tiptap/pm/model"; +import type { Transformer } from "./types.ts"; + +class Prosemirror implements Transformer { + defaultSchema: Schema = new Schema({ + nodes: { + text: {}, + doc: { content: "text*" }, + }, + }); + + schema(schema: Schema): Prosemirror { + this.defaultSchema = schema; + + return this; + } + + fromYdoc(document: Doc, fieldName?: string | Array): any { + const data = {}; + + // allow a single field name + if (typeof fieldName === "string") { + return yDocToProsemirrorJSON(document, fieldName); + } + + // default to all available fields if the given field name is empty + if (fieldName === undefined || fieldName.length === 0) { + fieldName = Array.from(document.share.keys()); + } + + fieldName.forEach((field) => { + // @ts-expect-error + data[field] = yDocToProsemirrorJSON(document, field); + }); + + return data; + } + + toYdoc( + document: any, + fieldName: string | Array = "prosemirror", + schema?: Schema, + ): Doc { + if (!document) { + throw new Error( + `You’ve passed an empty or invalid document to the Transformer. Make sure to pass ProseMirror-compatible JSON. Actually passed JSON: ${document}`, + ); + } + + // allow a single field name + if (typeof fieldName === "string") { + return prosemirrorJSONToYDoc( + schema || this.defaultSchema, + document, + fieldName, + ); + } + + const ydoc = new Doc(); + + fieldName.forEach((field) => { + const update = encodeStateAsUpdate( + prosemirrorJSONToYDoc(schema || this.defaultSchema, document, field), + ); + + applyUpdate(ydoc, update); + }); + + return ydoc; + } +} + +export const ProsemirrorTransformer = new Prosemirror(); diff --git a/packages/transformer/src/Tiptap.ts b/packages/transformer/src/Tiptap.ts new file mode 100644 index 0000000..2a44013 --- /dev/null +++ b/packages/transformer/src/Tiptap.ts @@ -0,0 +1,35 @@ +import type { Doc } from "yjs"; +// @ts-expect-error +import type { Extensions } from "@tiptap/core"; +import { getSchema } from "@tiptap/core"; +import { StarterKit } from "@tiptap/starter-kit"; +import type { Transformer } from "./types.ts"; +import { ProsemirrorTransformer } from "./Prosemirror.ts"; + +export class Tiptap implements Transformer { + defaultExtensions: Extensions = [StarterKit]; + + extensions(extensions: Extensions): Tiptap { + this.defaultExtensions = extensions; + + return this; + } + + fromYdoc(document: Doc, fieldName?: string | Array): any { + return ProsemirrorTransformer.fromYdoc(document, fieldName); + } + + toYdoc( + document: any, + fieldName: string | Array = "default", + extensions?: Extensions, + ): Doc { + return ProsemirrorTransformer.toYdoc( + document, + fieldName, + getSchema(extensions || this.defaultExtensions), + ); + } +} + +export const TiptapTransformer = new Tiptap(); diff --git a/packages/transformer/src/index.ts b/packages/transformer/src/index.ts new file mode 100644 index 0000000..a8debe5 --- /dev/null +++ b/packages/transformer/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Prosemirror.ts"; +export * from "./Tiptap.ts"; +export * from "./types.ts"; diff --git a/packages/transformer/src/types.ts b/packages/transformer/src/types.ts new file mode 100644 index 0000000..1d5fe9a --- /dev/null +++ b/packages/transformer/src/types.ts @@ -0,0 +1,6 @@ +import type { Doc } from "yjs"; + +export interface Transformer { + fromYdoc: (document: Doc, fieldName?: string | Array) => any; + toYdoc: (document: any, fieldName: string) => Doc; +} diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 0000000..5207754 --- /dev/null +++ b/playground/README.md @@ -0,0 +1,23 @@ +# Hocuspocus Playground + +``` +Hi! + +Welcome to the playground. This is mainly meant for internal usage during Hocuspocus development, +but feel free to fiddle around here and find a few examples of how things can work. + +Please note that there are different packages used in frontend / backend folders, so you should +run `pnpm install` in both if you get any errors. + +The playground is importing @hocuspocus packages from the packages folder, so you need to build them first. +Run `pnpm build:packages` in the repo root to do that. +If you want changes inside the packages folder to compile live, you can use rollup: `rollup -c -w`. + +(see also docs/contributing.md) + +You can run `pnpm playground` in the repository root, which will spin up a development server on +http://127.0.0.1:3000. + +If you have any questions, feel free to join our discord or ask on Github (links can be found in the +repo README.md one folder up). +``` diff --git a/playground/backend/.gitignore b/playground/backend/.gitignore new file mode 100644 index 0000000..cec1019 --- /dev/null +++ b/playground/backend/.gitignore @@ -0,0 +1,3 @@ +/database +/dashboard +/db.sqlite diff --git a/playground/backend/CHANGELOG.md b/playground/backend/CHANGELOG.md new file mode 100644 index 0000000..d422d43 --- /dev/null +++ b/playground/backend/CHANGELOG.md @@ -0,0 +1,973 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + + +### Bug Fixes + +* fixes hono types in playground ([#1045](https://github.com/ueberdosis/hocuspocus/issues/1045)) ([163fbed](https://github.com/ueberdosis/hocuspocus/commit/163fbed167222eac8e89095cca09074bb7ddf6e2)) + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + + +### Features + +* adds hono example to backend playground ([bb2d390](https://github.com/ueberdosis/hocuspocus/commit/bb2d3902fc406e2964f422eda52b4fcc035f734e)) +* playground ([ea54455](https://github.com/ueberdosis/hocuspocus/commit/ea544550b4b76f519a547008e3b77b76388996f2)) + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + + +### Bug Fixes + +* fixes koa example in playground ([1a73d14](https://github.com/ueberdosis/hocuspocus/commit/1a73d14221a006f38d07063d7001ae8f1e14deaf)) + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + + +### Bug Fixes + +* fixes express/koa backend demo, fixes [#939](https://github.com/ueberdosis/hocuspocus/issues/939) ([ddec715](https://github.com/ueberdosis/hocuspocus/commit/ddec7153e7d40725d6551d7dfbc61aac1b2b042e)) + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# 1.0.0-beta.111 (2022-09-28) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.2.6](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.5...@hocuspocus/server-demos@0.2.6) (2021-06-08) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.2.5](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.4...@hocuspocus/server-demos@0.2.5) (2021-05-15) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.2.4](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.3...@hocuspocus/server-demos@0.2.4) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.2.3](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.2...@hocuspocus/server-demos@0.2.3) (2021-04-20) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.2.2](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.1...@hocuspocus/server-demos@0.2.2) (2021-04-12) + + +### Bug Fixes + +* demos ([59b73de](https://github.com/ueberdosis/hocuspocus/commit/59b73de474f23b563995264f87a395e65ff73fee)) + + + + + +## [0.2.1](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.2.0...@hocuspocus/server-demos@0.2.1) (2021-04-08) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +# [0.2.0](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.1.37...@hocuspocus/server-demos@0.2.0) (2021-04-07) + + +### Features + +* add connect event to webhook extension ([d907354](https://github.com/ueberdosis/hocuspocus/commit/d907354561ca31130061b6393c240598d67259b1)) +* add create event to import documents from api ([240a032](https://github.com/ueberdosis/hocuspocus/commit/240a032a8d839144f9662c1ec947958823abfd45)) +* add disconnect event to webhook extension ([81679dd](https://github.com/ueberdosis/hocuspocus/commit/81679dd7828e96fc6e0a279880379eb16b28d788)) + + + + + +## [0.1.37](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.1.36...@hocuspocus/server-demos@0.1.37) (2021-04-06) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.1.36](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.1.35...@hocuspocus/server-demos@0.1.36) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## [0.1.35](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/server-demos@0.1.34...@hocuspocus/server-demos@0.1.35) (2021-03-29) + +**Note:** Version bump only for package @hocuspocus/server-demos + + + + + +## 0.1.34 (2021-03-18) + +**Note:** Version bump only for package @hocuspocus/server-demos diff --git a/playground/backend/package.json b/playground/backend/package.json new file mode 100644 index 0000000..d112e63 --- /dev/null +++ b/playground/backend/package.json @@ -0,0 +1,39 @@ +{ + "name": "@hocuspocus/server-demos", + "version": "3.4.6-rc.1", + "private": true, + "type": "module", + "scripts": { + "start": "node --conditions=source --experimental-transform-types", + "dev": "nodemon -e ts --watch ./ --watch ../../packages --exec pnpm start" + }, + "dependencies": { + "@hocuspocus/extension-logger": "workspace:*", + "@hocuspocus/extension-redis": "workspace:*", + "@hocuspocus/extension-sqlite": "workspace:*", + "@hocuspocus/extension-webhook": "workspace:*", + "@hocuspocus/server": "workspace:*", + "@hocuspocus/transformer": "workspace:*", + "@hono/node-server": "^1.19.17", + "@hono/node-ws": "^1.3.0", + "@tiptap/core": "^3.19.0", + "@tiptap/pm": "^3.19.0", + "@tiptap/starter-kit": "^3.19.0", + "@types/express": "^4.17.25", + "@types/express-ws": "^3.0.6", + "@types/node": "^16.18.126", + "cors": "^2.8.6", + "express": "^4.22.1", + "express-ws": "^5.0.2", + "hono": "^4.12.34", + "jsonwebtoken": "9.0.3", + "koa": "^3.1.2", + "koa-easy-ws": "^1.3.1", + "yjs": "^13.6.29" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.10", + "@types/koa": "^2.15.0", + "nodemon": "^3.1.14" + } +} diff --git a/playground/backend/src/bun.ts b/playground/backend/src/bun.ts new file mode 100644 index 0000000..d49055b --- /dev/null +++ b/playground/backend/src/bun.ts @@ -0,0 +1,59 @@ +import { Logger } from "@hocuspocus/extension-logger"; +import { Hocuspocus } from "@hocuspocus/server"; +import crossws from "crossws/adapters/bun"; + +const hocuspocus = new Hocuspocus({ + extensions: [new Logger()], +}); + +const ws = crossws({ + hooks: { + open(peer) { + // Use peer methods instead of peer.websocket to avoid + // Bun's Proxy `this` binding issue with ServerWebSocket + const wsLike = { + get readyState() { + return peer.websocket.readyState ?? 3; // 3 = CLOSED + }, + send(data: any) { + peer.send(data); + }, + close(code?: number, reason?: string) { + peer.close(code, reason); + }, + }; + const clientConnection = hocuspocus.handleConnection( + wsLike, + peer.request as Request, + ); + (peer as any)._hocuspocus = clientConnection; + }, + message(peer, message) { + (peer as any)._hocuspocus?.handleMessage(message.uint8Array()); + }, + close(peer, event) { + (peer as any)._hocuspocus?.handleClose({ + code: event.code, + reason: event.reason, + }); + }, + error(peer, error) { + console.error("WebSocket error for peer:", peer.id); + console.error(error); + }, + }, +}); + +Bun.serve({ + port: 8000, + websocket: ws.websocket, + fetch(request, server) { + if (request.headers.get("upgrade") === "websocket") { + return ws.handleUpgrade(request, server); + } + + return new Response("Welcome to Hocuspocus!"); + }, +}); + +console.log("Listening on ws://127.0.0.1:8000"); diff --git a/playground/backend/src/default.ts b/playground/backend/src/default.ts new file mode 100644 index 0000000..740d9ba --- /dev/null +++ b/playground/backend/src/default.ts @@ -0,0 +1,58 @@ +import { Logger } from "@hocuspocus/extension-logger"; +import { SQLite } from "@hocuspocus/extension-sqlite"; +import { Server } from "@hocuspocus/server"; + +const server = new Server({ + port: 8000, + address: "127.0.0.1", + name: "hocuspocus-fra1-01", + extensions: [ + new Logger(), + new SQLite({ + database: "db.sqlite", + }), + ], + + // async onAuthenticate(data) { + // if (data.token !== 'my-access-token') { + // throw new Error('Incorrect access token') + // } + // }, + + // Test error handling + // async onConnect(data) { + // throw new Error('CRASH') + // }, + + // async onConnect(data) { + // await new Promise((resolve, reject) => setTimeout(() => { + // // @ts-expect-error + // reject() + // }, 1337)) + // }, + + // async onConnect(data) { + // await new Promise((resolve, reject) => setTimeout(() => { + // // @ts-expect-error + // resolve() + // }, 1337)) + // }, + + // Intercept HTTP requests + // onRequest(data) { + // return new Promise((resolve, reject) => { + // const { response } = data + // // Respond with your custom content + // response.writeHead(200, { 'Content-Type': 'text/plain' }) + // response.end('This is my custom response, yay!') + + // // Rejecting the promise will stop the chain and no further + // // onRequest hooks are run + // return reject() + // }) + // }, +}); + +// server.enableMessageLogging() + +server.listen(); diff --git a/playground/backend/src/deno.ts b/playground/backend/src/deno.ts new file mode 100644 index 0000000..2054992 --- /dev/null +++ b/playground/backend/src/deno.ts @@ -0,0 +1,27 @@ +// @ts-nocheck - Deno types not installed +import { Hocuspocus } from "@hocuspocus/server"; +const hocuspocus = new Hocuspocus({ + name: "collaboration", +}); + +Deno.serve((req) => { + if (req.headers.get("upgrade") !== "websocket") { + return new Response(null, { status: 501 }); + } + + const { socket, response } = Deno.upgradeWebSocket(req); + + socket.binaryType = "arraybuffer"; + + const clientConnection = hocuspocus.handleConnection(socket, req); + + socket.addEventListener("message", (event) => { + clientConnection.handleMessage(new Uint8Array(event.data)); + }); + + socket.addEventListener("close", (event) => { + clientConnection.handleClose({ code: event.code, reason: event.reason }); + }); + + return response; +}); diff --git a/playground/backend/src/express.ts b/playground/backend/src/express.ts new file mode 100644 index 0000000..c163c85 --- /dev/null +++ b/playground/backend/src/express.ts @@ -0,0 +1,49 @@ +import { Logger } from "@hocuspocus/extension-logger"; +import { Hocuspocus, type WebSocketLike } from "@hocuspocus/server"; +import { createServer } from "node:http"; +import crossws from "crossws/adapters/node"; +import express from "express"; + +const hocuspocus = new Hocuspocus({ + extensions: [new Logger()], +}); + +const app = express(); + +app.get("/", (request, response) => { + response.send("Hello World!"); +}); + +const server = createServer(app); + +const ws = crossws({ + hooks: { + open(peer) { + const clientConnection = hocuspocus.handleConnection( + peer.websocket as unknown as WebSocketLike, + peer.request as Request, + { user_id: 1234 }, + ); + (peer as any)._hocuspocus = clientConnection; + }, + message(peer, message) { + (peer as any)._hocuspocus?.handleMessage(message.uint8Array()); + }, + close(peer, event) { + (peer as any)._hocuspocus?.handleClose({ + code: event.code, + reason: event.reason, + }); + }, + error(peer, error) { + console.error("WebSocket error for peer:", peer.id); + console.error(error); + }, + }, +}); + +server.on("upgrade", (request, socket, head) => { + ws.handleUpgrade(request, socket, head); +}); + +server.listen(1234, () => console.log("Listening on http://127.0.0.1:1234…")); diff --git a/playground/backend/src/hono.ts b/playground/backend/src/hono.ts new file mode 100644 index 0000000..1df2d3d --- /dev/null +++ b/playground/backend/src/hono.ts @@ -0,0 +1,51 @@ +// @ts-nocheck - Hono adapter types not installed +import { Hocuspocus } from "@hocuspocus/server"; +import { Hono } from "hono"; + +import { serve } from "@hono/node-server"; +import { createNodeWebSocket } from "@hono/node-ws"; + +const hocuspocus = new Hocuspocus({ + // … +}); + +const app = new Hono(); + +const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); + +app.get( + "/", + upgradeWebSocket((c) => { + let clientConnection; + return { + onOpen(_evt, ws) { + ws.raw.binaryType = "arraybuffer"; + clientConnection = hocuspocus.handleConnection(ws.raw, c.req.raw, {}); + }, + onMessage(evt) { + clientConnection?.handleMessage(new Uint8Array(evt.data)); + }, + onClose(_evt, ws) { + clientConnection?.handleClose(); + }, + }; + }), +); + +const server = serve( + { + fetch: app.fetch, + port: 8000, + }, + (info) => { + hocuspocus.hooks("onListen", { + instance: hocuspocus, + configuration: hocuspocus.configuration, + port: info.port, + }); + }, +); + +injectWebSocket(server); + +console.log("Hono server is running on ws://127.0.0.1:8000"); diff --git a/playground/backend/src/koa.ts b/playground/backend/src/koa.ts new file mode 100644 index 0000000..69b5daf --- /dev/null +++ b/playground/backend/src/koa.ts @@ -0,0 +1,49 @@ +import { Logger } from "@hocuspocus/extension-logger"; +import { Hocuspocus, type WebSocketLike } from "@hocuspocus/server"; +import { createServer } from "node:http"; +import crossws from "crossws/adapters/node"; +import Koa from "koa"; + +const hocuspocus = new Hocuspocus({ + extensions: [new Logger()], +}); + +const app = new Koa(); + +app.use(async (ctx) => { + ctx.body = "Hello World!"; +}); + +const server = createServer(app.callback()); + +const ws = crossws({ + hooks: { + open(peer) { + const clientConnection = hocuspocus.handleConnection( + peer.websocket as unknown as WebSocketLike, + peer.request as Request, + { user_id: 1234 }, + ); + (peer as any)._hocuspocus = clientConnection; + }, + message(peer, message) { + (peer as any)._hocuspocus?.handleMessage(message.uint8Array()); + }, + close(peer, event) { + (peer as any)._hocuspocus?.handleClose({ + code: event.code, + reason: event.reason, + }); + }, + error(peer, error) { + console.error("WebSocket error for peer:", peer.id); + console.error(error); + }, + }, +}); + +server.on("upgrade", (request, socket, head) => { + ws.handleUpgrade(request, socket, head); +}); + +server.listen(1234); diff --git a/playground/backend/src/load-document.ts b/playground/backend/src/load-document.ts new file mode 100644 index 0000000..d351bca --- /dev/null +++ b/playground/backend/src/load-document.ts @@ -0,0 +1,63 @@ +import type { onLoadDocumentPayload } from '@hocuspocus/server' +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { TiptapTransformer } from '@hocuspocus/transformer' +import { SQLite } from '@hocuspocus/extension-sqlite' + +const getProseMirrorJSON = (text: string) => { + return { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text, + }, + ], + }, + ], + } +} + +const server = new Server({ + port: 1234, + extensions: [ + new Logger(), + new SQLite({ + database: 'db.sqlite', + }), + ], + + async onConnect(data) { + await new Promise(resolve => setTimeout(() => { + // @ts-expect-error + resolve() + }, 1337)) + }, + + async onLoadDocument(data: onLoadDocumentPayload) { + if (data.document.isEmpty('default')) { + const defaultField = TiptapTransformer.toYdoc( + getProseMirrorJSON('What is love?'), + 'default', + ) + + data.document.merge(defaultField) + } + + if (data.document.isEmpty('secondary')) { + const secondaryField = TiptapTransformer.toYdoc( + getProseMirrorJSON('Baby don\'t hurt me…'), + 'secondary', + ) + + data.document.merge(secondaryField) + } + + return data.document + }, +}) + +server.listen() diff --git a/playground/backend/src/redis.ts b/playground/backend/src/redis.ts new file mode 100644 index 0000000..21f4fdb --- /dev/null +++ b/playground/backend/src/redis.ts @@ -0,0 +1,38 @@ +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { Redis } from '@hocuspocus/extension-redis' +import { SQLite } from '@hocuspocus/extension-sqlite' + +const server = new Server({ + port: 1234, + name: 'redis-1', + extensions: [ + new Logger(), + new Redis({ + host: '127.0.0.1', + port: 6379, + }), + new SQLite(), + ], +}) + +server.listen() + +const anotherServer = new Server({ + port: 1235, + name: 'redis-2', + extensions: [ + new Logger(), + new Redis({ + host: '127.0.0.1', + port: 6379, + }), + new SQLite(), + ], + + // onAwarenessUpdate: async ({ documentName, states }) => { + // console.log('onAwarenessUpdate', documentName, states) + // }, +}) + +anotherServer.listen() diff --git a/playground/backend/src/s3-redis.ts b/playground/backend/src/s3-redis.ts new file mode 100644 index 0000000..ca1ea11 --- /dev/null +++ b/playground/backend/src/s3-redis.ts @@ -0,0 +1,132 @@ +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { Redis } from '@hocuspocus/extension-redis' +import { S3 } from '@hocuspocus/extension-s3' + +// AWS S3 + Redis scaling example (only if AWS credentials are available) +let awsServer1, awsServer2; + +if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { + awsServer1 = new Server({ + port: 8001, + name: 's3-redis-aws-1', + extensions: [ + new Logger(), + new Redis({ + host: process.env.REDIS_HOST || '127.0.0.1', + port: Number(process.env.REDIS_PORT) || 6379, + }), + new S3({ + bucket: process.env.S3_BUCKET || 'my-hocuspocus-documents', + region: process.env.S3_REGION || 'us-east-1', + prefix: process.env.S3_PREFIX || 'collaborative-docs/', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } + }), + ], + }) + + // Second AWS S3 + Redis server instance + awsServer2 = new Server({ + port: 8002, + name: 's3-redis-aws-2', + extensions: [ + new Logger(), + new Redis({ + host: process.env.REDIS_HOST || '127.0.0.1', + port: Number(process.env.REDIS_PORT) || 6379, + }), + new S3({ + bucket: process.env.S3_BUCKET || 'my-hocuspocus-documents', + region: process.env.S3_REGION || 'us-east-1', + prefix: process.env.S3_PREFIX || 'collaborative-docs/', + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } + }), + ], + }) +} + +// MinIO + Redis local development example +const minioServer1 = new Server({ + port: 8000, + name: 's3-redis-minio-1', + extensions: [ + new Logger(), + new Redis({ + host: process.env.REDIS_HOST || '127.0.0.1', + port: Number(process.env.REDIS_PORT) || 6379, + }), + new S3({ + bucket: process.env.S3_BUCKET || 'hocuspocus-documents', + endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', + forcePathStyle: true, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'minioadmin', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'minioadmin' + } + }), + ], +}) + +const minioServer2 = new Server({ + port: 8003, + name: 's3-redis-minio-2', + extensions: [ + new Logger(), + new Redis({ + host: process.env.REDIS_HOST || '127.0.0.1', + port: Number(process.env.REDIS_PORT) || 6379, + }), + new S3({ + bucket: process.env.S3_BUCKET || 'hocuspocus-documents', + endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', + forcePathStyle: true, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'minioadmin', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'minioadmin' + } + }), + ], +}) + +// Start server instances +if (awsServer1 && awsServer2) { + awsServer1.listen() + awsServer2.listen() +} +minioServer1.listen() +minioServer2.listen() + +const awsStatus = (awsServer1 && awsServer2) ? ` + AWS S3 Instances: + Server 1: http://localhost:8001 + Server 2: http://localhost:8002` : ` + AWS S3 Instances: [Skipped - no AWS credentials found]` + +console.log(` +🚀 S3 + Redis Scaling Examples Started: +${awsStatus} + + MinIO Instances: + Server 1: http://localhost:8000 + Server 2: http://localhost:8003 + +📝 Environment Variables: + REDIS_HOST, REDIS_PORT + S3_BUCKET, S3_REGION, S3_ENDPOINT, S3_PREFIX + AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY + +🔄 Redis handles real-time sync between instances +💾 S3/MinIO provides persistent document storage + +🐳 For local development with Redis + MinIO: + docker-compose up redis minio + +💡 Documents are stored as: {prefix}{documentName}.bin +🔗 All server instances share the same Redis and S3 backend +`) diff --git a/playground/backend/src/s3.ts b/playground/backend/src/s3.ts new file mode 100644 index 0000000..eee64dd --- /dev/null +++ b/playground/backend/src/s3.ts @@ -0,0 +1,103 @@ +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { S3 } from '@hocuspocus/extension-s3' + +// Basic AWS S3 example +const server = new Server({ + port: 8001, + name: 's3-aws', + extensions: [ + new Logger(), + new S3({ + bucket: process.env.S3_BUCKET || 'my-hocuspocus-documents', + region: process.env.S3_REGION || 'us-east-1', + credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } : undefined + }), + ], +}) + +server.listen() + +// MinIO S3-compatible example (Main development server) +const minioServer = new Server({ + port: 8000, + name: 's3-minio', + extensions: [ + new Logger(), + new S3({ + bucket: process.env.S3_BUCKET || 'hocuspocus-documents', + endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', + forcePathStyle: true, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'minioadmin', + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'minioadmin' + } + }), + ], +}) + +minioServer.listen() + +// AWS S3 with IAM roles (no credentials needed when running on EC2/Lambda) +// Falls back to environment variables for local development +const iamServer = new Server({ + port: 8002, + name: 's3-iam', + extensions: [ + new Logger(), + new S3({ + bucket: process.env.S3_BUCKET || 'my-hocuspocus-documents', + region: process.env.S3_REGION || 'us-east-1', + prefix: process.env.S3_PREFIX || 'collaborative-docs/', + // Use credentials from environment variables for local development + credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } : undefined + }), + ], +}) + +iamServer.listen() + +// DigitalOcean Spaces example +const spacesServer = new Server({ + port: 8003, + name: 's3-spaces', + extensions: [ + new Logger(), + new S3({ + bucket: process.env.S3_BUCKET || 'my-spaces-bucket', + region: process.env.S3_REGION || 'nyc3', + endpoint: process.env.S3_ENDPOINT || 'https://nyc3.digitaloceanspaces.com', + credentials: { + accessKeyId: process.env.SPACES_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID || 'your-spaces-key', + secretAccessKey: process.env.SPACES_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY || 'your-spaces-secret' + } + }), + ], +}) + +spacesServer.listen() + +console.log(` +🚀 S3 Extension Examples Started: + + MinIO (Main): http://localhost:8000 + AWS S3: http://localhost:8001 + IAM Roles: http://localhost:8002 + DigitalOcean: http://localhost:8003 + +📝 Environment Variables: + S3_BUCKET, S3_REGION, S3_ENDPOINT, S3_PREFIX + AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY + SPACES_ACCESS_KEY, SPACES_SECRET_KEY + +🐳 For local development with MinIO: + ./start-dev.sh + +💡 Documents are stored as: {prefix}{documentName}.bin +`) diff --git a/playground/backend/src/slow.ts b/playground/backend/src/slow.ts new file mode 100644 index 0000000..8ac2bd2 --- /dev/null +++ b/playground/backend/src/slow.ts @@ -0,0 +1,25 @@ +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { SQLite } from '@hocuspocus/extension-sqlite' + +const server = new Server({ + port: 1234, + extensions: [ + new Logger(), + new SQLite({ + database: 'db.sqlite', + }), + ], + + async onConnect(data) { + // simulate a very slow authentication process that takes 10 seconds (or more if you want to type more) + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + await new Promise((resolve: Function) => { + setTimeout(() => { resolve() }, 10000) + }) + + return true + }, +}) + +server.listen() diff --git a/playground/backend/src/tiptapcollab.ts b/playground/backend/src/tiptapcollab.ts new file mode 100644 index 0000000..cd198b0 --- /dev/null +++ b/playground/backend/src/tiptapcollab.ts @@ -0,0 +1,21 @@ +import express from 'express' +import expressWebsockets from 'express-ws' +// @ts-expect-error +import jsonwebtoken from 'jsonwebtoken' +// @ts-expect-error +import cors from 'cors' + +const { app } = expressWebsockets(express()) +app.use(cors()) + +app.get('/', (request, response) => { + // do NOT do this in production, this is just for demo purposes. The secret MUST be stored on the server and never reach the client side. + const { secret } = request.query + + const jwt = jsonwebtoken.sign({ + allowedDocumentNames: ['test1', 'test2'], + }, secret?.toString() ?? '') + + response.send(jwt) +}) +app.listen(1234, () => console.log('Listening on http://127.0.0.1:1234…')) diff --git a/playground/backend/src/webhook.ts b/playground/backend/src/webhook.ts new file mode 100644 index 0000000..18021ac --- /dev/null +++ b/playground/backend/src/webhook.ts @@ -0,0 +1,134 @@ +import type { IncomingMessage, ServerResponse, Server as HTTPServer} from 'http' +import { + createServer, +} from 'http' +import { createHmac, timingSafeEqual } from 'crypto' +import { Server } from '@hocuspocus/server' +import { Logger } from '@hocuspocus/extension-logger' +import { TiptapTransformer } from '@hocuspocus/transformer' +import { Events, Webhook } from '@hocuspocus/extension-webhook' + +/* + * Setup server + */ +const server = new Server({ + port: 1234, + extensions: [ + new Logger(), + new Webhook({ + transformer: TiptapTransformer, + secret: '1234', + url: 'http://localhost:12345', + events: [ + Events.onCreate, Events.onChange, Events.onConnect, + ], + }), + ], +}) + +server.listen() + +/* + * Setup receiver + */ +class WebhookReceiver { + + secret = '1234' + + apiToken = '123456' + + server: HTTPServer + + constructor() { + this.server = createServer(this.handleRequest.bind(this)) + + this.server.listen(12345, () => { + console.log('[WebhookReceiver] listening on port 12345…') + }) + } + + verifySignature(body: string, header: string): boolean { + const signature = Buffer.from(header) + + const hmac = createHmac('sha256', this.secret) + const digest = Buffer.from(`sha256=${hmac.update(body).digest('hex')}`) + + return signature.length !== digest.length || timingSafeEqual(digest, signature) + } + + handleRequest(request: IncomingMessage, response: ServerResponse) { + let data = '' + + request.on('data', chunk => { + data += chunk + }) + + request.on('end', () => { + if (!this.verifySignature(data, request.headers['x-hocuspocus-signature-256'])) { + response.writeHead(403, 'signature not valid') + } + + const { event, payload } = <{ event: string, payload: any }> JSON.parse(data) + + try { + // @ts-expect-error - let me do some magic here please TypeScript + this[`on${event[0].toUpperCase()}${event.substr(1)}`](payload, response) + } catch (e) { + console.log(`[WebhookReceiver] unknown event "${event}"`) + } + }) + } + + onConnect(payload: any, response: ServerResponse) { + console.log(`[WebhookReceiver] user connected to ${payload.documentName}`) + + // authorize user + if (payload.requestParameters?.token !== this.apiToken) { + response.writeHead(403, 'unathorized') + return response.end() + } + + // return context + response.writeHead(200, { 'Content-Type': 'application/json' }) + response.end(JSON.stringify({ + user: { + id: 1, + name: 'John', + }, + })) + } + + onCreate(payload: any, response: ServerResponse) { + console.log(`[WebhookReceiver] document ${payload.documentName} created`) + + // return a document for the "default" field + response.writeHead(200, { 'Content-Type': 'application/json' }) + response.end(JSON.stringify({ + default: + { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'What is love?', + }, + ], + }, + ], + }, + })) + } + + onChange(payload: any, response: ServerResponse) { + console.log(`[WebhookReceiver] document ${payload.documentName} was changed: ${JSON.stringify(payload.document)}`) + } + + onDisconnect(payload: any, response: ServerResponse) { + console.log(`[WebhookReceiver] user ${payload.context.user.name} disconnected from ${payload.documentName}`) + } +} + +const receiver = new WebhookReceiver() diff --git a/playground/frontend/.gitignore b/playground/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/playground/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/playground/frontend/CHANGELOG.md b/playground/frontend/CHANGELOG.md new file mode 100644 index 0000000..7459dcc --- /dev/null +++ b/playground/frontend/CHANGELOG.md @@ -0,0 +1,302 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + + +### Bug Fixes + +* fixes playground plug and play confg ([f9dff84](https://github.com/ueberdosis/hocuspocus/commit/f9dff843e5132fd858ce410e1774089e6b4c9692)) + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + + +### Bug Fixes + +* playground: Cannot update a component (`CollaborationStatus`) while rendering a different component (`CollaborativeEditor`) ([9c6b87d](https://github.com/ueberdosis/hocuspocus/commit/9c6b87d882d2a1c00dca55cac1d71de2221a3a6d)) +* playground: destroy instead of detach ([c8b60f8](https://github.com/ueberdosis/hocuspocus/commit/c8b60f84ea7b7df0d04a9aad71e92e8bba2c747c)) +* playground: port 1234 ([62e45d4](https://github.com/ueberdosis/hocuspocus/commit/62e45d40fd7e044daa6b0eee0b96b0786d2c5a6b)) +* playground: update styling of collaboration caret ([4a2506b](https://github.com/ueberdosis/hocuspocus/commit/4a2506ba0fe399280b7cd6cba31602c0660e7a36)) + + +### Features + +* playground: adds two editors for nicer testing ([b2332f2](https://github.com/ueberdosis/hocuspocus/commit/b2332f21c5369a6e980e19c1ef7842e8cf32b878)) + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/frontend-demo + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + +**Note:** Version bump only for package @hocuspocus/frontend-demo diff --git a/playground/frontend/README.md b/playground/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/playground/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/playground/frontend/app/SocketContext1.ts b/playground/frontend/app/SocketContext1.ts new file mode 100644 index 0000000..a3ca30d --- /dev/null +++ b/playground/frontend/app/SocketContext1.ts @@ -0,0 +1,8 @@ +"use client"; + +import type { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { createContext } from "react"; + +export const SocketContext1 = createContext( + null, +); diff --git a/playground/frontend/app/SocketContext2.ts b/playground/frontend/app/SocketContext2.ts new file mode 100644 index 0000000..ba496d8 --- /dev/null +++ b/playground/frontend/app/SocketContext2.ts @@ -0,0 +1,8 @@ +"use client"; + +import type { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { createContext } from "react"; + +export const SocketContext2 = createContext( + null, +); diff --git a/playground/frontend/app/articles/[slug]/ArticleEditor.tsx b/playground/frontend/app/articles/[slug]/ArticleEditor.tsx new file mode 100644 index 0000000..ae302c3 --- /dev/null +++ b/playground/frontend/app/articles/[slug]/ArticleEditor.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { SocketContext1 } from "@/app/SocketContext1"; +import { SocketContext2 } from "@/app/SocketContext2"; +import CollaborationStatus from "@/app/articles/[slug]/CollaborationStatus"; +import CollaborativeEditor from "@/app/articles/[slug]/CollaborativeEditor"; +import { + HocuspocusProvider, + HocuspocusProviderWebsocket, +} from "@hocuspocus/provider"; +// import { TiptapCollabProvider } from "@tiptap-cloud/provider"; +import { useContext, useEffect, useState } from "react"; + +export default function ArticleEditor({ slug }: { slug: string }) { + const socket1 = useContext(SocketContext1); + const socket2 = useContext(SocketContext2); + + const [provider1, setProvider1] = useState(); + const [provider2, setProvider2] = useState(); + + useEffect(() => { + if (!socket1 || !socket2) return; + + // Create separate providers for each editor + const _p1 = new HocuspocusProvider({ + websocketProvider: socket1, + name: slug, + onOpen: (data) => console.log("Editor 1 onOpen!", data), + onClose: (data) => console.log("Editor 1 onClose!", data), + onAuthenticated: (data) => console.log("Editor 1 onAuthenticated!", data), + onAuthenticationFailed: (data) => + console.log("Editor 1 onAuthenticationFailed", data), + onUnsyncedChanges: (data) => + console.log("Editor 1 onUnsyncedChanges", data), + onStatus(data) { + console.log("editor 1 onStatus", data); + }, + }); + + const _p2 = new HocuspocusProvider({ + websocketProvider: socket2, + name: slug, + onOpen: (data) => console.log("Editor 2 onOpen!", data), + onClose: (data) => console.log("Editor 2 onClose!", data), + onAuthenticated: (data) => console.log("Editor 2 onAuthenticated!", data), + onAuthenticationFailed: (data) => + console.log("Editor 2 onAuthenticationFailed", data), + onUnsyncedChanges: (data) => + console.log("Editor 2 onUnsyncedChanges", data), + onStatus(data) { + console.log("editor 2 onStatus", data); + }, + }); + + setProvider1(_p1); + setProvider2(_p2); + + return () => { + _p1.destroy(); + _p2.destroy(); + }; + }, [socket1, socket2, slug]); + + if (!provider1 || !provider2) { + return <>; + } + + // you need to attach here, to make sure the connection gets properly established due to React strict-mode re-run of hooks + provider1.attach(); + provider2.attach(); + + return ( +
+ {/* Header */} +
+
+
+
+

+ Article #{slug} +

+

+ Real-time collaborative editing - Two editors demonstrating sync +

+
+
+
+
+ + {/* Main content */} +
+
+ {/* Editors side by side */} +
+ {/* Editor 1 */} +
+
+

+ Editor 1 +

+ + Independent WebSocket connection + +
+ + {/* Status Panel 1 */} + + + {/* Editor 1 */} +
+ +
+
+ + {/* Editor 2 */} +
+
+

+ Editor 2 +

+ + Independent WebSocket connection + +
+ + {/* Status Panel 2 */} + + + {/* Editor 2 */} +
+ +
+
+
+ + {/* Info panel */} +
+
+
+
+ + + +
+
+
+

+ Real-time Collaboration Demo +

+

+ Both editors are connected to the same document but use + separate WebSocket connections. Changes made in one editor + will appear in the other in real-time. Try typing in either + editor or use the connection controls to test synchronization + behavior. +

+
+
+
+
+
+
+ ); +} diff --git a/playground/frontend/app/articles/[slug]/CollaborationStatus.tsx b/playground/frontend/app/articles/[slug]/CollaborationStatus.tsx new file mode 100644 index 0000000..8ccb950 --- /dev/null +++ b/playground/frontend/app/articles/[slug]/CollaborationStatus.tsx @@ -0,0 +1,184 @@ +import type { + HocuspocusProvider, + onStatusParameters, + onUnsyncedChangesParameters, +} from "@hocuspocus/provider"; +import { useEffect, useState } from "react"; + +const CollaborationStatus = (props: { + provider: HocuspocusProvider; +}) => { + const { provider } = props; + + const [unsyncedChanges, setUnsyncedChanges] = useState(0); + const [socketStatus, setSocketStatus] = useState(null); + const [isAttached, _setAttached] = useState(false); + + useEffect(() => { + setSocketStatus(provider.configuration.websocketProvider.status); + _setAttached(provider.isAttached); + + const handleUnsyncedChanges = (changes: onUnsyncedChangesParameters) => { + setUnsyncedChanges(changes.number); + }; + + const handleSocketStatus = (status: onStatusParameters) => { + setSocketStatus(status.status); + }; + + provider.on("unsyncedChanges", handleUnsyncedChanges); + provider.configuration.websocketProvider.on("status", handleSocketStatus); + + return () => { + provider.off("unsyncedChanges", handleUnsyncedChanges); + provider.configuration.websocketProvider.off( + "status", + handleSocketStatus, + ); + }; + }, [provider]); + + const updateAttached = (attach: boolean) => { + if (attach) { + provider.attach(); + } else { + provider.detach(); + } + + _setAttached(provider.isAttached); + }; + + const getStatusColor = (status: string | null) => { + switch (status) { + case "connected": + return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"; + case "connecting": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"; + case "disconnected": + return "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"; + default: + return "bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400"; + } + }; + + const UNSYNCED_THRESHOLD = 10; + + return ( +
+ {/* Alert banner when unsynced changes grow — indicates connection issues */} + {unsyncedChanges >= UNSYNCED_THRESHOLD && ( +
+
+ + + +
+
+

+ Connection issue detected +

+

+ {unsyncedChanges} changes are waiting to sync. Your edits are + saved locally but not yet on the server. +

+
+
+ )} + + {/* Status indicators */} +
+

+ Connection Status +

+
+
+
+ + {socketStatus || "Unknown"} + +
+
+
+ + {isAttached ? "Attached" : "Detached"} + +
+
+
= UNSYNCED_THRESHOLD ? "bg-red-500 animate-pulse" : "bg-orange-500 animate-pulse"}`} + >
+ = UNSYNCED_THRESHOLD ? "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400" : "bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400"}`} + > + {unsyncedChanges} unsynced + +
+
+
+ + {/* Control buttons */} +
+
+

+ WebSocket Controls +

+
+ + +
+
+ +
+

+ Provider Controls +

+
+ + +
+
+
+
+ ); +}; + +export default CollaborationStatus; diff --git a/playground/frontend/app/articles/[slug]/CollaborativeEditor.tsx b/playground/frontend/app/articles/[slug]/CollaborativeEditor.tsx new file mode 100644 index 0000000..2f5a437 --- /dev/null +++ b/playground/frontend/app/articles/[slug]/CollaborativeEditor.tsx @@ -0,0 +1,65 @@ +"use client"; + +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import { Collaboration } from "@tiptap/extension-collaboration"; +import { CollaborationCaret } from "@tiptap/extension-collaboration-caret"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { StarterKit } from "@tiptap/starter-kit"; +import * as Y from "yjs"; + +const initialContent = [ + 1, 3, 223, 175, 255, 141, 2, 0, 7, 1, 7, 100, 101, 102, 97, 117, 108, 116, 3, + 9, 112, 97, 114, 97, 103, 114, 97, 112, 104, 7, 0, 223, 175, 255, 141, 2, 0, + 6, 4, 0, 223, 175, 255, 141, 2, 1, 17, 72, 101, 108, 108, 111, 32, 87, 111, + 114, 108, 100, 33, 32, 240, 159, 140, 142, 0, +]; + +const CollaborativeEditor = (props: { + slug: string; + provider: HocuspocusProvider; +}) => { + /** + * if you want to load initial content to the editor, the safest way to do so is by applying an initial Yjs update. + * Yjs updates can safely be applied multiple times, while using `setContent` or similar Tiptap commands may result in + * duplicate content in the Tiptap editor. + * + * The easiest way to generate the Yjs update (`initialContent` above) is to do something like + * + * ``` + * console.log(Y.encodeStateAsUpdate(provider.props.document).toString()) + * ``` + * + * after you have filled the editor with the desired content. + */ + Y.applyUpdate(props.provider.document, Uint8Array.from(initialContent)); + + const editor = useEditor( + { + extensions: [ + // make sure to turn off the undo-redo extension when using collaboration + StarterKit.configure({ + undoRedo: false, + }), + Collaboration.configure({ + document: props.provider.document, + }), + CollaborationCaret.configure({ + provider: props.provider, + }), + ], + // immediatelyRender needs to be `false` when using SSR + immediatelyRender: false, + editorProps: { + attributes: { + class: + "prose prose-lg max-w-none focus:outline-none min-h-[500px] p-8 bg-white/50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm", + }, + }, + }, + [props.provider.document], + ); + + return ; +}; + +export default CollaborativeEditor; diff --git a/playground/frontend/app/articles/[slug]/page.tsx b/playground/frontend/app/articles/[slug]/page.tsx new file mode 100644 index 0000000..7b18648 --- /dev/null +++ b/playground/frontend/app/articles/[slug]/page.tsx @@ -0,0 +1,10 @@ +import ArticleEditor from "@/app/articles/[slug]/ArticleEditor"; + +export default async function Home(props: { + params: Promise<{ slug: string }>; +}) { + const params = await props.params; + const slug = params.slug; + + return ; +} diff --git a/playground/frontend/app/articles/layout.tsx b/playground/frontend/app/articles/layout.tsx new file mode 100644 index 0000000..4e8f565 --- /dev/null +++ b/playground/frontend/app/articles/layout.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +// import { +// TiptapCollabProvider, +// TiptapCollabProviderWebsocket, +// } from "@tiptap-cloud/provider"; +import { useEffect, useState } from "react"; +import { SocketContext1 } from "@/app/SocketContext1"; +import { SocketContext2 } from "@/app/SocketContext2"; + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const [socket1, setSocket1] = useState( + null, + ); + const [socket2, setSocket2] = useState( + null, + ); + + useEffect(() => { + const newlyCreatedSocket1 = new HocuspocusProviderWebsocket({ + url: "ws://localhost:8000", + }); + const newlyCreatedSocket2 = new HocuspocusProviderWebsocket({ + url: "ws://localhost:8000", + }); + // const newlyCreatedSocket = new TiptapCollabProviderWebsocket({ + // appId: "", + // }); + + setSocket1(newlyCreatedSocket1); + setSocket2(newlyCreatedSocket2); + + return () => { + newlyCreatedSocket1?.destroy(); + newlyCreatedSocket2?.destroy(); + }; + }, []); + + if (socket1 && socket2) { + return ( + <> + + {children} + + + ); + } +} diff --git a/playground/frontend/app/favicon.ico b/playground/frontend/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/playground/frontend/app/favicon.ico differ diff --git a/playground/frontend/app/globals.css b/playground/frontend/app/globals.css new file mode 100644 index 0000000..6326aa1 --- /dev/null +++ b/playground/frontend/app/globals.css @@ -0,0 +1,124 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; + line-height: 1.6; +} + +/* Modern editor styles */ +.tiptap { + :first-child { + margin-top: 0; + } + + /* Placeholder styling */ + p.is-editor-empty:first-child::before { + color: rgb(148 163 184); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + font-style: italic; + } + + p { + word-break: break-word; + line-height: 1.8; + } + + /* Improved focus styles */ + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); + } + + /* Better typography */ + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 600; + color: rgb(15 23 42); + } + + @media (prefers-color-scheme: dark) { + h1, + h2, + h3, + h4, + h5, + h6 { + color: rgb(248 250 252); + } + p.is-editor-empty:first-child::before { + color: rgb(100 116 139); + } + } + + /* Modern collaboration cursor */ + .collaboration-carets__caret { + border-left: 2px solid currentColor; + border-right: 2px solid currentColor; + margin-left: -1px; + margin-right: -1px; + pointer-events: none; + position: relative; + word-break: normal; + animation: blink 1s infinite; + } + + /* Smooth blinking animation */ + @keyframes blink { + 0%, + 50% { + opacity: 1; + } + 51%, + 100% { + opacity: 0.3; + } + } + + /* Modern username label */ + .collaboration-carets__label { + border-radius: 6px 6px 6px 2px; + color: white; + font-size: 11px; + font-style: normal; + font-weight: 500; + left: -1px; + line-height: normal; + padding: 0.25rem 0.5rem; + position: absolute; + top: -1.8em; + user-select: none; + white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(4px); + } +} diff --git a/playground/frontend/app/layout.tsx b/playground/frontend/app/layout.tsx new file mode 100644 index 0000000..f1c0f91 --- /dev/null +++ b/playground/frontend/app/layout.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import "./globals.css"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+ {/* Sidebar */} +
+
+
+ +
+ H +
+ Hocuspocus + +
+ + +
+
+ + {/* Main content */} +
+ {children} +
+
+ + + ); +} diff --git a/playground/frontend/app/page.tsx b/playground/frontend/app/page.tsx new file mode 100644 index 0000000..7fe101a --- /dev/null +++ b/playground/frontend/app/page.tsx @@ -0,0 +1,76 @@ +export default function Home() { + return ( +
+ {/* Header */} +
+
+

+ Hocuspocus v3 Playground +

+

+ Real-time collaborative editor demo +

+
+
+ + {/* Main content */} +
+
+ {/* Welcome card */} +
+
+
+ 🚀 +
+

Welcome!

+
+

+ This demo showcases how to use a shared WebSocket connection to load multiple documents with real-time collaboration. +

+
+

+ 💡 Tip: Open a second tab or window and experience collaborative editing in real-time! +

+
+
+ + {/* Features grid */} +
+
+
+ 🔌 +
+

Smart Connection

+

+ WebSocket connection opens when you enter article routes and closes when you leave. +

+
+ +
+
+ 📄 +
+

On-Demand Loading

+

+ Documents are fetched as needed via the shared connection for optimal performance. +

+
+
+ + {/* Debug info */} +
+
+
+ 🔍 +
+

Developer Console

+
+

+ Open your browser's developer console to see detailed connection and synchronization information. +

+
+
+
+
+ ); +} diff --git a/playground/frontend/app/react-provider/[slug]/ArticleEditor.tsx b/playground/frontend/app/react-provider/[slug]/ArticleEditor.tsx new file mode 100644 index 0000000..ce02b6b --- /dev/null +++ b/playground/frontend/app/react-provider/[slug]/ArticleEditor.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { + HocuspocusProviderWebsocketComponent, + HocuspocusRoom, +} from "@hocuspocus/provider-react"; +import React, { useCallback } from "react"; +import CollaborationStatus from "./CollaborationStatus"; +import CollaborativeEditor from "./CollaborativeEditor"; +import ConnectedUsers from "./ConnectedUsers"; + +export default function ArticleEditor({ slug }: { slug: string }) { + const handleAuthFailed = useCallback( + (data: { reason: string }) => { + console.error(`[Editor 1] Auth failed for "${slug}":`, data.reason); + }, + [slug], + ); + + return ( +
+ {/* Header */} +
+
+
+
+

+ Article #{slug} + + provider-react + +

+

+ Using @hocuspocus/provider-react hooks and components +

+
+
+
+
+ + {/* Main content */} +
+
+ {/* Two independent WebSocket connections, each with its own room */} +
+ {/* Editor 1 — event handlers via HocuspocusRoom props */} + + + console.log("[Editor 1] Connection closed:", data.event) + } + onSynced={(data) => + console.log("[Editor 1] Synced:", data.state) + } + > +
+
+

+ Editor 1 +

+ + Event handlers via props + +
+ + + + + +
+ +
+
+
+
+ + {/* Editor 2 — event handlers via useHocuspocusEvent hook (see CollaborationStatus) */} + + +
+
+

+ Editor 2 +

+ + Event handlers via hook + +
+ + + + + +
+ +
+
+
+
+
+ + {/* Info panel */} +
+
+
+
+ + + +
+
+
+

+ @hocuspocus/provider-react Demo +

+

+ This demo uses the new React-specific package. Each editor is + wrapped in its own{" "} + + HocuspocusProviderWebsocketComponent + {" "} + (independent WebSocket) and{" "} + + HocuspocusRoom + + . Status is read via{" "} + + useHocuspocusConnectionStatus() + + ,{" "} + + useHocuspocusSyncStatus() + + , and{" "} + + useHocuspocusAwareness() + {" "} + hooks — no manual event listeners needed. +

+
+
+
+
+
+
+ ); +} diff --git a/playground/frontend/app/react-provider/[slug]/CollaborationStatus.tsx b/playground/frontend/app/react-provider/[slug]/CollaborationStatus.tsx new file mode 100644 index 0000000..f6e5c47 --- /dev/null +++ b/playground/frontend/app/react-provider/[slug]/CollaborationStatus.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { + useHocuspocusConnectionStatus, + useHocuspocusEvent, + useHocuspocusProvider, + useHocuspocusSyncStatus, +} from "@hocuspocus/provider-react"; +import { useState } from "react"; + +const CollaborationStatus = () => { + const provider = useHocuspocusProvider(); + const connectionStatus = useHocuspocusConnectionStatus(); + const syncStatus = useHocuspocusSyncStatus(); + const [unsyncedChanges, setUnsyncedChanges] = useState(0); + + // Using the useHocuspocusEvent hook instead of manual provider.on/off + useHocuspocusEvent("unsyncedChanges", (data) => { + setUnsyncedChanges(data.number); + }); + + const getStatusColor = (status: string) => { + switch (status) { + case "connected": + return "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"; + case "connecting": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"; + case "disconnected": + return "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"; + default: + return "bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400"; + } + }; + + const UNSYNCED_THRESHOLD = 10; + + return ( +
+ {/* Alert banner when unsynced changes grow — indicates connection issues */} + {unsyncedChanges >= UNSYNCED_THRESHOLD && ( +
+
+ + + +
+
+

+ Connection issue detected +

+

+ {unsyncedChanges} changes are waiting to sync. Your edits are + saved locally but not yet on the server. +

+
+
+ )} + + {/* Status indicators */} +
+

+ Connection Status +

+
+
+
+ + {connectionStatus} + +
+
+
+ + {syncStatus} + +
+
+
= UNSYNCED_THRESHOLD ? "bg-red-500 animate-pulse" : "bg-orange-500 animate-pulse"}`} + /> + = UNSYNCED_THRESHOLD ? "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400" : "bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400"}`} + > + {unsyncedChanges} unsynced + +
+
+
+ + {/* Control buttons */} +
+
+

+ WebSocket Controls +

+
+ + +
+
+ +
+

+ Provider Controls +

+
+ + +
+
+
+
+ ); +}; + +export default CollaborationStatus; diff --git a/playground/frontend/app/react-provider/[slug]/CollaborativeEditor.tsx b/playground/frontend/app/react-provider/[slug]/CollaborativeEditor.tsx new file mode 100644 index 0000000..bc90dd9 --- /dev/null +++ b/playground/frontend/app/react-provider/[slug]/CollaborativeEditor.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useHocuspocusProvider } from "@hocuspocus/provider-react"; +import { Collaboration } from "@tiptap/extension-collaboration"; +import { CollaborationCaret } from "@tiptap/extension-collaboration-caret"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { StarterKit } from "@tiptap/starter-kit"; +import { useEffect, useRef } from "react"; +import * as Y from "yjs"; + +const initialContent = [ + 1, 3, 223, 175, 255, 141, 2, 0, 7, 1, 7, 100, 101, 102, 97, 117, 108, 116, 3, + 9, 112, 97, 114, 97, 103, 114, 97, 112, 104, 7, 0, 223, 175, 255, 141, 2, 0, + 6, 4, 0, 223, 175, 255, 141, 2, 1, 17, 72, 101, 108, 108, 111, 32, 87, 111, + 114, 108, 100, 33, 32, 240, 159, 140, 142, 0, +]; + +const CollaborativeEditor = () => { + const provider = useHocuspocusProvider(); + const initializedRef = useRef(false); + + useEffect(() => { + if (!initializedRef.current) { + Y.applyUpdate(provider.document, Uint8Array.from(initialContent)); + initializedRef.current = true; + } + }, [provider.document]); + + const editor = useEditor( + { + extensions: [ + StarterKit.configure({ + undoRedo: false, + }), + Collaboration.configure({ + document: provider.document, + }), + CollaborationCaret.configure({ + provider, + }), + ], + immediatelyRender: false, + editorProps: { + attributes: { + class: + "prose prose-lg max-w-none focus:outline-none min-h-[500px] p-8 bg-white/50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm", + }, + }, + }, + [provider.document], + ); + + return ; +}; + +export default CollaborativeEditor; diff --git a/playground/frontend/app/react-provider/[slug]/ConnectedUsers.tsx b/playground/frontend/app/react-provider/[slug]/ConnectedUsers.tsx new file mode 100644 index 0000000..569063e --- /dev/null +++ b/playground/frontend/app/react-provider/[slug]/ConnectedUsers.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useHocuspocusAwareness } from "@hocuspocus/provider-react"; +import { useEffect, useState } from "react"; + +const colors = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-pink-500", + "bg-amber-500", + "bg-cyan-500", + "bg-rose-500", + "bg-indigo-500", +]; + +const ConnectedUsers = () => { + const users = useHocuspocusAwareness(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return ( +
+
+ + Users + + {mounted && ( + <> +
+ {users.map((user, i) => ( +
+ + {String(user.clientId).slice(-2)} + +
+ ))} +
+ + {users.length} connected + + + )} +
+
+ ); +}; + +export default ConnectedUsers; diff --git a/playground/frontend/app/react-provider/[slug]/page.tsx b/playground/frontend/app/react-provider/[slug]/page.tsx new file mode 100644 index 0000000..7ba309b --- /dev/null +++ b/playground/frontend/app/react-provider/[slug]/page.tsx @@ -0,0 +1,10 @@ +import ArticleEditor from "./ArticleEditor"; + +export default async function Page(props: { + params: Promise<{ slug: string }>; +}) { + const params = await props.params; + const slug = params.slug; + + return ; +} diff --git a/playground/frontend/app/react-provider/layout.tsx b/playground/frontend/app/react-provider/layout.tsx new file mode 100644 index 0000000..3ee935c --- /dev/null +++ b/playground/frontend/app/react-provider/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return children; +} diff --git a/playground/frontend/next.config.ts b/playground/frontend/next.config.ts new file mode 100644 index 0000000..1fb148c --- /dev/null +++ b/playground/frontend/next.config.ts @@ -0,0 +1,20 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + serverExternalPackages: ["yjs"], + turbopack: { + root: "../../", + resolveAlias: { + "@hocuspocus/provider": "../../packages/provider/src/index.ts", + "@hocuspocus/common": "../../packages/common/src/index.ts", + "@hocuspocus/transformer": "../../packages/transformer/src/index.ts", + "@hocuspocus/provider-react": + "../../packages/provider-react/src/index.ts", + }, + }, + webpack: (config) => { + return config; + }, +}; + +export default nextConfig; diff --git a/playground/frontend/package.json b/playground/frontend/package.json new file mode 100644 index 0000000..843ec46 --- /dev/null +++ b/playground/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "@hocuspocus/frontend-demo", + "version": "3.4.6-rc.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hocuspocus/provider": "workspace:*", + "@hocuspocus/provider-react": "workspace:*", + "@tiptap/extension-collaboration": "^3.19.0", + "@tiptap/extension-collaboration-caret": "^3.19.0", + "@tiptap/extension-floating-menu": "^3.19.0", + "@tiptap/react": "^3.19.0", + "@tiptap/starter-kit": "^3.19.0", + "jwt-encode": "^1.0.1", + "next": "16.3.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "yjs": "^13.6.29" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "^22.19.11", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "tailwindcss": "^4.1.18", + "typescript": "^5.8.2" + } +} diff --git a/playground/frontend/postcss.config.mjs b/playground/frontend/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/playground/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/playground/frontend/public/file.svg b/playground/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/playground/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/frontend/public/globe.svg b/playground/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/playground/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/frontend/public/next.svg b/playground/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/playground/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/frontend/public/vercel.svg b/playground/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/playground/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/frontend/public/window.svg b/playground/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/playground/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/frontend/tsconfig.json b/playground/frontend/tsconfig.json new file mode 100644 index 0000000..e7ff3a2 --- /dev/null +++ b/playground/frontend/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..82b5834 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11020 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@hono/node-server@<2.0.5': ^2.0.5 + brace-expansion@>=4.0.0 <5.0.9: ^5.0.9 + fast-xml-parser@<5.7.0: ^5.7.0 + fast-xml-parser@>=5.0.0 <5.3.5: ^5.3.5 + fast-xml-parser@>=5.0.0 <5.3.6: ^5.3.6 + fast-xml-parser@>=5.0.0 <5.3.8: ^5.3.8 + fast-xml-parser@>=5.0.0 <5.5.6: ^5.5.6 + fast-xml-parser@>=5.0.0 <5.5.7: ^5.5.7 + js-yaml@>=4.0.0 <4.3.0: ^4.3.0 + js-yaml@>=4.0.0 <=4.1.1: ^4.1.2 + minimatch@>=10.0.0 <10.2.1: ^10.2.1 + minimatch@>=10.0.0 <10.2.3: ^10.2.3 + postcss@<8.5.10: ^8.5.10 + postcss@<=8.5.11: ^8.5.12 + postcss@<=8.5.17: ^8.5.18 + postcss@<=8.5.22: ^8.5.23 + qs@>=6.11.1 <=6.15.1: ^6.15.2 + sharp@<0.35.0: ^0.35.0 + tar@<=7.5.10: ^7.5.11 + tar@<=7.5.15: ^7.5.16 + tar@<=7.5.16: ^7.5.17 + tar@<=7.5.17: ^7.5.18 + tar@<=7.5.18: ^7.5.19 + tar@<=7.5.20: ^7.5.21 + tar@<=7.5.9: ^7.5.10 + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: 2.4.5 + version: 2.4.5 + '@types/bun': + specifier: ^1.3.9 + version: 1.3.9 + ava: + specifier: ^7.0.0 + version: 7.0.0(@ava/typescript@3.0.1)(encoding@0.1.13)(supports-color@5.5.0) + concurrently: + specifier: ^6.5.1 + version: 6.5.1 + lerna: + specifier: ^9.0.5 + version: 9.0.5(@types/node@22.19.11)(supports-color@7.2.0) + nx: + specifier: ^22.7.8 + version: 22.7.8 + rolldown: + specifier: 1.0.0-rc.6 + version: 1.0.0-rc.6 + rolldown-plugin-dts: + specifier: ^0.22.3 + version: 0.22.3(rolldown@1.0.0-rc.6)(typescript@5.8.2) + typescript: + specifier: 5.8.2 + version: 5.8.2 + + packages/cli: + dependencies: + '@hocuspocus/extension-logger': + specifier: workspace:^ + version: link:../extension-logger + '@hocuspocus/extension-s3': + specifier: workspace:^ + version: link:../extension-s3 + '@hocuspocus/extension-sqlite': + specifier: workspace:^ + version: link:../extension-sqlite + '@hocuspocus/extension-webhook': + specifier: workspace:^ + version: link:../extension-webhook + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + meow: + specifier: ^14.0.0 + version: 14.0.0 + + packages/common: + dependencies: + lib0: + specifier: ^0.2.117 + version: 0.2.117 + + packages/extension-database: + dependencies: + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + yjs: + specifier: ^13.6.8 + version: 13.6.29 + + packages/extension-logger: + dependencies: + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + + packages/extension-redis: + dependencies: + '@hocuspocus/common': + specifier: workspace:^ + version: link:../common + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + '@sesamecare-oss/redlock': + specifier: ^1.4.0 + version: 1.4.0(ioredis@5.6.1(supports-color@5.5.0)) + ioredis: + specifier: ~5.6.1 + version: 5.6.1(supports-color@5.5.0) + kleur: + specifier: ^4.1.5 + version: 4.1.5 + y-protocols: + specifier: ^1.0.6 + version: 1.0.7(yjs@13.6.29) + yjs: + specifier: ^13.6.8 + version: 13.6.29 + + packages/extension-s3: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.990.0 + version: 3.990.0 + '@hocuspocus/extension-database': + specifier: workspace:^ + version: link:../extension-database + kleur: + specifier: ^4.1.5 + version: 4.1.5 + + packages/extension-sqlite: + dependencies: + '@hocuspocus/extension-database': + specifier: workspace:^ + version: link:../extension-database + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + kleur: + specifier: ^4.1.5 + version: 4.1.5 + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + + packages/extension-throttle: + dependencies: + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + + packages/extension-webhook: + dependencies: + '@hocuspocus/common': + specifier: workspace:^ + version: link:../common + '@hocuspocus/server': + specifier: workspace:^ + version: link:../server + '@hocuspocus/transformer': + specifier: workspace:^ + version: link:../transformer + yjs: + specifier: ^13.6.8 + version: 13.6.29 + + packages/provider: + dependencies: + '@hocuspocus/common': + specifier: workspace:^ + version: link:../common + '@lifeomic/attempt': + specifier: ^3.1.0 + version: 3.1.0 + lib0: + specifier: ^0.2.117 + version: 0.2.117 + y-protocols: + specifier: ^1.0.6 + version: 1.0.7(yjs@13.6.29) + yjs: + specifier: ^13.6.8 + version: 13.6.29 + + packages/provider-react: + dependencies: + '@hocuspocus/provider': + specifier: workspace:^ + version: link:../provider + yjs: + specifier: ^13.6.8 + version: 13.6.29 + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + react: + specifier: ^19.2.4 + version: 19.2.4 + + packages/server: + dependencies: + '@hocuspocus/common': + specifier: workspace:^ + version: link:../common + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 + crossws: + specifier: ^0.4.4 + version: 0.4.4 + kleur: + specifier: ^4.1.5 + version: 4.1.5 + lib0: + specifier: ^0.2.117 + version: 0.2.117 + y-protocols: + specifier: ^1.0.6 + version: 1.0.7(yjs@13.6.29) + yjs: + specifier: ^13.6.8 + version: 13.6.29 + + packages/transformer: + dependencies: + '@tiptap/core': + specifier: ^3.0.1 + version: 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/starter-kit': + specifier: ^3.19.0 + version: 3.19.0 + y-prosemirror: + specifier: ^1.2.1 + version: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + yjs: + specifier: ^13.6.8 + version: 13.6.29 + devDependencies: + '@tiptap/pm': + specifier: ^3.19.0 + version: 3.19.0 + + playground/backend: + dependencies: + '@hocuspocus/extension-logger': + specifier: workspace:* + version: link:../../packages/extension-logger + '@hocuspocus/extension-redis': + specifier: workspace:* + version: link:../../packages/extension-redis + '@hocuspocus/extension-sqlite': + specifier: workspace:* + version: link:../../packages/extension-sqlite + '@hocuspocus/extension-webhook': + specifier: workspace:* + version: link:../../packages/extension-webhook + '@hocuspocus/server': + specifier: workspace:* + version: link:../../packages/server + '@hocuspocus/transformer': + specifier: workspace:* + version: link:../../packages/transformer + '@hono/node-server': + specifier: ^2.0.5 + version: 2.0.12(hono@4.13.5) + '@hono/node-ws': + specifier: ^1.3.0 + version: 1.3.0(@hono/node-server@2.0.12(hono@4.13.5))(hono@4.13.5) + '@tiptap/core': + specifier: ^3.19.0 + version: 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': + specifier: ^3.19.0 + version: 3.19.0 + '@tiptap/starter-kit': + specifier: ^3.19.0 + version: 3.19.0 + '@types/express': + specifier: ^4.17.25 + version: 4.17.25 + '@types/express-ws': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^16.18.126 + version: 16.18.126 + cors: + specifier: ^2.8.6 + version: 2.8.6 + express: + specifier: ^4.22.1 + version: 4.22.1(supports-color@5.5.0) + express-ws: + specifier: ^5.0.2 + version: 5.0.2(express@4.22.1(supports-color@5.5.0)) + hono: + specifier: ^4.12.34 + version: 4.13.5 + jsonwebtoken: + specifier: 9.0.3 + version: 9.0.3 + koa: + specifier: ^3.1.2 + version: 3.1.2 + koa-easy-ws: + specifier: ^1.3.1 + version: 1.3.1(supports-color@5.5.0) + yjs: + specifier: ^13.6.29 + version: 13.6.29 + devDependencies: + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/koa': + specifier: ^2.15.0 + version: 2.15.0 + nodemon: + specifier: ^3.1.14 + version: 3.1.14 + + playground/frontend: + dependencies: + '@hocuspocus/provider': + specifier: workspace:* + version: link:../../packages/provider + '@hocuspocus/provider-react': + specifier: workspace:* + version: link:../../packages/provider-react + '@tiptap/extension-collaboration': + specifier: ^3.19.0 + version: 3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) + '@tiptap/extension-collaboration-caret': + specifier: ^3.19.0 + version: 3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)) + '@tiptap/extension-floating-menu': + specifier: ^3.19.0 + version: 3.19.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/react': + specifier: ^3.19.0 + version: 3.19.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/starter-kit': + specifier: ^3.19.0 + version: 3.19.0 + jwt-encode: + specifier: ^1.0.1 + version: 1.0.1 + next: + specifier: 16.3.3 + version: 16.3.3(@types/node@22.19.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + yjs: + specifier: ^13.6.29 + version: 13.6.29 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4.1.18 + version: 4.1.18 + '@types/node': + specifier: ^22.19.11 + version: 22.19.11 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + typescript: + specifier: ^5.8.2 + version: 5.8.2 + + tests: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.990.0 + version: 3.990.0 + '@hocuspocus/common': + specifier: workspace:^ + version: link:../packages/common + '@hocuspocus/extension-database': + specifier: workspace:^ + version: link:../packages/extension-database + '@hocuspocus/extension-logger': + specifier: workspace:^ + version: link:../packages/extension-logger + '@hocuspocus/extension-redis': + specifier: workspace:^ + version: link:../packages/extension-redis + '@hocuspocus/extension-s3': + specifier: workspace:^ + version: link:../packages/extension-s3 + '@hocuspocus/extension-throttle': + specifier: workspace:^ + version: link:../packages/extension-throttle + '@hocuspocus/provider': + specifier: workspace:^ + version: link:../packages/provider + '@hocuspocus/server': + specifier: workspace:^ + version: link:../packages/server + '@hocuspocus/transformer': + specifier: workspace:^ + version: link:../packages/transformer + redis: + specifier: ^4.7.1 + version: 4.7.1 + sinon: + specifier: ^12.0.1 + version: 12.0.1 + ws: + specifier: ^8.21.0 + version: 8.21.0 + yjs: + specifier: ^13.6.29 + version: 13.6.29 + devDependencies: + '@types/redis': + specifier: ^4.0.11 + version: 4.0.11 + '@types/sinon': + specifier: ^10.0.20 + version: 10.0.20 + lib0: + specifier: ^0.2.117 + version: 0.2.117 + mockdate: + specifier: ^3.0.5 + version: 3.0.5 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ava/typescript@3.0.1': + resolution: {integrity: sha512-/JXIUuKsvkaneaiA9ckk3ksFTqvu0mDNlChASrTe2BnDsvMbhQdPWyqQjJ9WRJWVhhs5TWn1/0Pp1G6Rv8Syrw==} + engines: {node: '>=12.22 <13 || >=14.17 <15 || >=16.4 <17 || >=17'} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.990.0': + resolution: {integrity: sha512-XnsM8RgB35Atn2+aYSocitCybDG82x9yYf/s2D23ytpyHCupmuZN3LzK2a0WxmKO6Zf7EtEIYy0mHGY4tLp9YA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sso@3.990.0': + resolution: {integrity: sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.10': + resolution: {integrity: sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.0': + resolution: {integrity: sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.8': + resolution: {integrity: sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.10': + resolution: {integrity: sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.8': + resolution: {integrity: sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.8': + resolution: {integrity: sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.9': + resolution: {integrity: sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.8': + resolution: {integrity: sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.8': + resolution: {integrity: sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.8': + resolution: {integrity: sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.3': + resolution: {integrity: sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.3': + resolution: {integrity: sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.972.8': + resolution: {integrity: sha512-Hn6gumcN/3/8Fzo9z7N1pA2PRfE8S+qAqdb4g3MqzXjIOIe+VxD7edO/DKAJ1YH11639EGQIHBz0wdOb5btjtw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.3': + resolution: {integrity: sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.3': + resolution: {integrity: sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.3': + resolution: {integrity: sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.3': + resolution: {integrity: sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.10': + resolution: {integrity: sha512-wLkB4bshbBtsAiC2WwlHzOWXu1fx3ftL63fQl0DxEda48Q6B8bcHydZppE3KjEIpPyiNOllByfSnb07cYpIgmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.3': + resolution: {integrity: sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.10': + resolution: {integrity: sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.990.0': + resolution: {integrity: sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.3': + resolution: {integrity: sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.990.0': + resolution: {integrity: sha512-O55s1eFmKi+2Ko5T1hbdxL6tFVONGscSVe9VRxS4m91Tlbo9iG2Q2HvKWq1DuKQAuUWSUfMmjrRt07JNzizr2A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.990.0': + resolution: {integrity: sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.1': + resolution: {integrity: sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.2': + resolution: {integrity: sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.990.0': + resolution: {integrity: sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.4': + resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.3': + resolution: {integrity: sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==} + + '@aws-sdk/util-user-agent-node@3.972.8': + resolution: {integrity: sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.4': + resolution: {integrity: sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@8.0.0-rc.2': + resolution: {integrity: sha512-oCQ1IKPwkzCeJzAPb7Fv8rQ9k5+1sG8mf2uoHiMInPYvkRfrDJxbTIbH51U+jstlkghus0vAi3EBvkfvEsYNLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-string-parser@8.0.0-rc.2': + resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.0-rc.2': + resolution: {integrity: sha512-xExUBkuXWJjVuIbO7z6q7/BA9bgfJDEhVL0ggrggLMbg0IzCUWGT1hZGE8qUH7Il7/RD/a6cZ3AAFrrlp1LF/A==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/parser@8.0.0-rc.2': + resolution: {integrity: sha512-29AhEtcq4x8Dp3T72qvUMZHx0OMXCj4Jy/TEReQa+KWLln524Cj1fWb3QFi0l/xSpptQBR6y9RNEXuxpFvwiUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/types@8.0.0-rc.2': + resolution: {integrity: sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@biomejs/biome@2.4.5': + resolution: {integrity: sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.5': + resolution: {integrity: sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.5': + resolution: {integrity: sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.5': + resolution: {integrity: sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.5': + resolution: {integrity: sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.5': + resolution: {integrity: sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.5': + resolution: {integrity: sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.5': + resolution: {integrity: sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.5': + resolution: {integrity: sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + + '@hono/node-ws@1.3.0': + resolution: {integrity: sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q==} + engines: {node: '>=18.14.1'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + '@hono/node-server': ^2.0.5 + hono: ^4.6.0 + + '@hutson/parse-repository-url@3.0.2': + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/string-locale-compare@1.1.0': + resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lerna/create@9.0.5': + resolution: {integrity: sha512-Gwd6ooSqXMdkdhiCGvHAfLRstj7W3ttr72WQB3Jf9HPP1A6mWtw0O80D0X+T/2hakqfe7lNLuKrEid4f7C0qbg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: This package is an implementation detail of Lerna and is no longer published separately. + + '@lifeomic/attempt@3.1.0': + resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==} + + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@next/env@16.3.3': + resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} + + '@next/swc-darwin-arm64@16.3.3': + resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.3': + resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.3': + resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.3.3': + resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.3.3': + resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.3.3': + resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.3.3': + resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.3': + resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodable/entities@3.0.0': + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/agent@4.0.0': + resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/arborist@9.1.6': + resolution: {integrity: sha512-c5Pr3EG8UP5ollkJy2x+UdEQC5sEHe3H9whYn6hb2HJimAKS4zmoJkx5acCiR/g4P38RnCSMlsYQyyHnKYeLvQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + '@npmcli/fs@4.0.0': + resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/git@6.0.3': + resolution: {integrity: sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/git@7.0.1': + resolution: {integrity: sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/installed-package-contents@3.0.0': + resolution: {integrity: sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + '@npmcli/installed-package-contents@4.0.0': + resolution: {integrity: sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + '@npmcli/map-workspaces@5.0.3': + resolution: {integrity: sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/metavuln-calculator@9.0.3': + resolution: {integrity: sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/name-from-folder@3.0.0': + resolution: {integrity: sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/name-from-folder@4.0.0': + resolution: {integrity: sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/node-gyp@4.0.0': + resolution: {integrity: sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/node-gyp@5.0.0': + resolution: {integrity: sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/package-json@7.0.2': + resolution: {integrity: sha512-0ylN3U5htO1SJTmy2YI78PZZjLkKUGg7EKgukb2CRi0kzyoDr0cfjHAzi7kozVhj2V3SxN1oyKqZ2NSo40z00g==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/promise-spawn@8.0.3': + resolution: {integrity: sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/promise-spawn@9.0.1': + resolution: {integrity: sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/query@4.0.1': + resolution: {integrity: sha512-4OIPFb4weUUwkDXJf4Hh1inAn8neBGq3xsH4ZsAaN6FK3ldrFkH7jSpCc7N9xesi0Sp+EBXJ9eGMDrEww2Ztqw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/redact@3.2.2': + resolution: {integrity: sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/run-script@10.0.3': + resolution: {integrity: sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@nx/devkit@22.5.1': + resolution: {integrity: sha512-1ZJ8pCB+6EWC8X6q8tfBweg92WzFAwliBhtBkOPP8Li8GQq71ulPWRdY4lDd5pH3Ea1zKqhOtBKWdOlyDPOKYA==} + peerDependencies: + nx: '>= 21 <= 23 || ^22.0.0-0' + + '@nx/nx-darwin-arm64@22.7.8': + resolution: {integrity: sha512-IM1geDyWPFsS565de9dByYNZ5I3j8FQZvNUp9LIYw1dNu70sCWbAT0glw0anholOwlAb7JWEscoUeV7ouRBW0A==} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@22.7.8': + resolution: {integrity: sha512-X/AyooJCmAwHyp9f//bspkAmtaPsv2lPEKe6OiOScsyxJITP4nw+rOfIAJ4Ar64WQcMAY8WLP+ys4ah0K6DZSw==} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@22.7.8': + resolution: {integrity: sha512-mgdqiFag8txon4XugDtNj+hq+X9Whn8LBMuqwJSez93L1fralzpQFSUip7XnE7ejQRr5Zewg3BFscGlsk8Cy3A==} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@22.7.8': + resolution: {integrity: sha512-g7ojIloHGFI7wuMnUdg7io42EL7C7ae4zAolNVj7eXZM54amn3qoNjwbyqaVbBQvUNRiAKJizGyn1IhKpzvG9A==} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@22.7.8': + resolution: {integrity: sha512-Kws8e7W4epfqpTWYaV7KLKaj33o+9JtJa2rErx/XFTtMXhfVzOs5hC4oIrZsYpgk1DuT0APp7UDvX06q2kdAVQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-arm64-musl@22.7.8': + resolution: {integrity: sha512-fpnyVFL+mSqLdKBPk8+n/rHUEXiem7Xwr9cIOd2Dd5zxQ5wcwj4qSYTNRsBPI2qDFo3esHliwaoFJZULbTHldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@nx/nx-linux-x64-gnu@22.7.8': + resolution: {integrity: sha512-KRbkSthClEwkbpt/LLJmBJhIOvOsyrp9OICisF9p3mOODjaxAuYmyOgrVreg09BT2F4hXN3JXpvt9eIZpTCRsw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-x64-musl@22.7.8': + resolution: {integrity: sha512-pdPMWko1yZI5ZNI6/t2Y5rQPGgV/ah5DW+mlHo2aFKav4lnxvgisEh9e4HtOsYBfK/9PyYZ5u3M9hLSaMiJ75w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@nx/nx-win32-arm64-msvc@22.7.8': + resolution: {integrity: sha512-yYDu3pj7AXu7LtN/T/bA4TMWWWbmvEE4wa8UW8ZU5JeWYblyXQA7HyYpPAb4fLzCtHb/dIrNDghVBRIeAAcnSw==} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@22.7.8': + resolution: {integrity: sha512-cSr0qMt/GgM2aOBFsapxllnIv55j0gAz/6eWvqEOx5sS8d3X1G0IgazZnPbjW2c8gfSYaBZ9UmYnfapWIO/jqg==} + cpu: [x64] + os: [win32] + + '@octokit/auth-token@4.0.0': + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@9.0.6': + resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} + engines: {node: '>= 18'} + + '@octokit/graphql@7.1.1': + resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/plugin-enterprise-rest@6.0.1': + resolution: {integrity: sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==} + + '@octokit/plugin-paginate-rest@11.4.4-cjs.2': + resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + + '@octokit/plugin-request-log@4.0.1': + resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '5' + + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1': + resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5 + + '@octokit/request-error@5.1.1': + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} + + '@octokit/request@8.4.1': + resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} + engines: {node: '>= 18'} + + '@octokit/rest@20.1.2': + resolution: {integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@rolldown/binding-android-arm64@1.0.0-rc.6': + resolution: {integrity: sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.6': + resolution: {integrity: sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.6': + resolution: {integrity: sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.6': + resolution: {integrity: sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6': + resolution: {integrity: sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6': + resolution: {integrity: sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.6': + resolution: {integrity: sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.6': + resolution: {integrity: sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.6': + resolution: {integrity: sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.6': + resolution: {integrity: sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.6': + resolution: {integrity: sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6': + resolution: {integrity: sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.6': + resolution: {integrity: sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.6': + resolution: {integrity: sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@sesamecare-oss/redlock@1.4.0': + resolution: {integrity: sha512-2z589R+yxKLN4CgKxP1oN4dsg6Y548SE4bVYam/R0kHk7Q9VrQ9l66q+k1ehhSLLY4or9hcchuF9/MhuuZdjJg==} + engines: {node: '>=16'} + peerDependencies: + ioredis: '>=5' + + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/core@3.2.1': + resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/protobuf-specs@0.5.0': + resolution: {integrity: sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@sigstore/sign@4.1.1': + resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/tuf@4.0.2': + resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/verify@3.1.1': + resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sinonjs/commons@1.8.6': + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@11.3.1': + resolution: {integrity: sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==} + + '@sinonjs/fake-timers@8.1.0': + resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} + + '@sinonjs/samsam@6.1.3': + resolution: {integrity: sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==} + + '@sinonjs/text-encoding@0.7.3': + resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} + deprecated: |- + Deprecated: no longer maintained and no longer used by Sinon packages. See + https://github.com/sinonjs/nise/issues/243 for replacement details. + + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.0': + resolution: {integrity: sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.8': + resolution: {integrity: sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.8': + resolution: {integrity: sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.8': + resolution: {integrity: sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.8': + resolution: {integrity: sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.8': + resolution: {integrity: sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.9': + resolution: {integrity: sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.8': + resolution: {integrity: sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.8': + resolution: {integrity: sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.14': + resolution: {integrity: sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.31': + resolution: {integrity: sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.10': + resolution: {integrity: sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.11.3': + resolution: {integrity: sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.30': + resolution: {integrity: sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.33': + resolution: {integrity: sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.12': + resolution: {integrity: sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.8': + resolution: {integrity: sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@tiptap/core@3.19.0': + resolution: {integrity: sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==} + peerDependencies: + '@tiptap/pm': ^3.19.0 + + '@tiptap/core@3.30.5': + resolution: {integrity: sha512-3O7N0FyKIfuLV+xrdWyDM3V5eUY/q2CgLjhhMwOAbM1Pu7VPp9VP+TpEYOdH8aRyB+h1vj5hX5A747D8ZrPfHA==} + peerDependencies: + '@tiptap/pm': 3.30.5 + + '@tiptap/extension-blockquote@3.19.0': + resolution: {integrity: sha512-y3UfqY9KD5XwWz3ndiiJ089Ij2QKeiXy/g1/tlAN/F1AaWsnkHEHMLxCP1BIqmMpwsX7rZjMLN7G5Lp7c9682A==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-bold@3.19.0': + resolution: {integrity: sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-bubble-menu@3.19.0': + resolution: {integrity: sha512-klNVIYGCdznhFkrRokzGd6cwzoi8J7E5KbuOfZBwFwhMKZhlz/gJfKmYg9TJopeUhrr2Z9yHgWTk8dh/YIJCdQ==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-bullet-list@3.19.0': + resolution: {integrity: sha512-F9uNnqd0xkJbMmRxVI5RuVxwB9JaCH/xtRqOUNQZnRBt7IdAElCY+Dvb4hMCtiNv+enGM/RFGJuFHR9TxmI7rw==} + peerDependencies: + '@tiptap/extension-list': ^3.19.0 + + '@tiptap/extension-code-block@3.19.0': + resolution: {integrity: sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-code@3.19.0': + resolution: {integrity: sha512-2kqqQIXBXj2Or+4qeY3WoE7msK+XaHKL6EKOcKlOP2BW8eYqNTPzNSL+PfBDQ3snA7ljZQkTs/j4GYDj90vR1A==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-collaboration-caret@3.19.0': + resolution: {integrity: sha512-YMHQ7ZxdWaGzR+k0UWP7eHo+jrUbrx2C3gTEGYpR2YMGN38yuV5Yelwzd2rflaedccJrFsYwMWxRRs5ygTGGUw==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + '@tiptap/y-tiptap': ^3.0.2 + + '@tiptap/extension-collaboration@3.19.0': + resolution: {integrity: sha512-Cb4RXo2C05w44OsT22weLYqf2mnyTacvtz3iWYswgq1slMOl4Gs5RQE+jHgyvjVbhj34yPS6ghoWBBrriX9a1w==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + '@tiptap/y-tiptap': ^3.0.2 + yjs: ^13 + + '@tiptap/extension-document@3.19.0': + resolution: {integrity: sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-dropcursor@3.19.0': + resolution: {integrity: sha512-sf3dEZXiLvsGqVK2maUIzXY6qtYYCvBumag7+VPTMGQ0D4hiZ1X/4ukt4+6VXDg5R2WP1CoIt/QvUetUjWNhbQ==} + peerDependencies: + '@tiptap/extensions': ^3.19.0 + + '@tiptap/extension-floating-menu@3.19.0': + resolution: {integrity: sha512-JaoEkVRkt+Slq3tySlIsxnMnCjS0L5n1CA1hctjLy0iah8edetj3XD5mVv5iKqDzE+LIjF4nwLRRVKJPc8hFBg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-gapcursor@3.19.0': + resolution: {integrity: sha512-w7DACS4oSZaDWjz7gropZHPc9oXqC9yERZTcjWxyORuuIh1JFf0TRYspleK+OK28plK/IftojD/yUDn1MTRhvA==} + peerDependencies: + '@tiptap/extensions': ^3.19.0 + + '@tiptap/extension-hard-break@3.19.0': + resolution: {integrity: sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-heading@3.19.0': + resolution: {integrity: sha512-uLpLlfyp086WYNOc0ekm1gIZNlEDfmzOhKzB0Hbyi6jDagTS+p9mxUNYeYOn9jPUxpFov43+Wm/4E24oY6B+TQ==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-horizontal-rule@3.19.0': + resolution: {integrity: sha512-iqUHmgMGhMgYGwG6L/4JdelVQ5Mstb4qHcgTGd/4dkcUOepILvhdxajPle7OEdf9sRgjQO6uoAU5BVZVC26+ng==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-italic@3.19.0': + resolution: {integrity: sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-link@3.19.0': + resolution: {integrity: sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-list-item@3.19.0': + resolution: {integrity: sha512-VsSKuJz4/Tb6ZmFkXqWpDYkRzmaLTyE6dNSEpNmUpmZ32sMqo58mt11/huADNwfBFB0Ve7siH/VnFNIJYY3xvg==} + peerDependencies: + '@tiptap/extension-list': ^3.19.0 + + '@tiptap/extension-list-keymap@3.19.0': + resolution: {integrity: sha512-bxgmAgA3RzBGA0GyTwS2CC1c+QjkJJq9hC+S6PSOWELGRiTbwDN3MANksFXLjntkTa0N5fOnL27vBHtMStURqw==} + peerDependencies: + '@tiptap/extension-list': ^3.19.0 + + '@tiptap/extension-list@3.19.0': + resolution: {integrity: sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/extension-ordered-list@3.19.0': + resolution: {integrity: sha512-cxGsINquwHYE1kmhAcLNLHAofmoDEG6jbesR5ybl7tU5JwtKVO7S/xZatll2DU1dsDAXWPWEeeMl4e/9svYjCg==} + peerDependencies: + '@tiptap/extension-list': ^3.19.0 + + '@tiptap/extension-paragraph@3.19.0': + resolution: {integrity: sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-strike@3.19.0': + resolution: {integrity: sha512-xYpabHsv7PccLUBQaP8AYiFCnYbx6P93RHPd0lgNwhdOjYFd931Zy38RyoxPHAgbYVmhf1iyx7lpuLtBnhS5dA==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-text@3.19.0': + resolution: {integrity: sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extension-underline@3.19.0': + resolution: {integrity: sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw==} + peerDependencies: + '@tiptap/core': ^3.19.0 + + '@tiptap/extensions@3.19.0': + resolution: {integrity: sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + + '@tiptap/pm@3.19.0': + resolution: {integrity: sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==} + + '@tiptap/react@3.19.0': + resolution: {integrity: sha512-GQQMUUXMpNd8tRjc1jDK3tDRXFugJO7C928EqmeBcBzTKDrFIJ3QUoZKEPxUNb6HWhZ2WL7q00fiMzsv4DNSmg==} + peerDependencies: + '@tiptap/core': ^3.19.0 + '@tiptap/pm': ^3.19.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.19.0': + resolution: {integrity: sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug==} + + '@tiptap/y-tiptap@3.0.2': + resolution: {integrity: sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@tufjs/models@4.1.0': + resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/accepts@1.3.7': + resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bun@1.3.9': + resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/content-disposition@0.5.9': + resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} + + '@types/cookies@0.9.2': + resolution: {integrity: sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express-ws@3.0.6': + resolution: {integrity: sha512-6ZDt+tMEQgM4RC1sMX1fIO7kHQkfUDlWfxoPddXUeeDjmc+Yt/fCzqXfp8rFahNr5eIxdomrWphLEWDkB2q3UQ==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/keygrip@1.0.6': + resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + + '@types/koa-compose@3.2.9': + resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==} + + '@types/koa@2.15.0': + resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/redis@4.0.11': + resolution: {integrity: sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==} + deprecated: This is a stub types definition. redis provides its own type definitions, so you do not need this installed. + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/sinon@10.0.20': + resolution: {integrity: sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==} + + '@types/sinonjs__fake-timers@15.0.1': + resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vercel/nft@1.3.2': + resolution: {integrity: sha512-HC8venRc4Ya7vNeBsJneKHHMDDWpQie7VaKhAIOst3MKO+DES+Y/SbzSp8mFkD7OzwAE2HhHkeSuSmwS20mz3A==} + engines: {node: '>=20'} + hasBin: true + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + + array-find-index@1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + arrgv@1.0.2: + resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} + engines: {node: '>=8.0.0'} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + arrify@3.0.0: + resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} + engines: {node: '>=12'} + + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} + engines: {node: '>=20.19.0'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + ava@7.0.0: + resolution: {integrity: sha512-4sRJO/gehlfAgSbuH02mClDDiyymnuFmirE3KqPXl2pic1FaFTZaAACKqr85WT4o08iLjViMR9gmMkxzbZ3AgA==} + engines: {node: ^20.19 || ^22.20 || ^24.12 || >=25} + hasBin: true + peerDependencies: + '@ava/typescript': '*' + peerDependenciesMeta: + '@ava/typescript': + optional: true + + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + bin-links@5.0.0: + resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} + engines: {node: ^18.17.0 || >=20.5.0} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bun-types@1.3.9: + resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} + + byte-size@8.1.1: + resolution: {integrity: sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==} + engines: {node: '>=12.17'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacache@20.0.3: + resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} + engines: {node: ^20.17.0 || >=22.9.0} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + callsites@4.2.0: + resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} + engines: {node: '>=12.20'} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + cbor@10.0.11: + resolution: {integrity: sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==} + engines: {node: '>=20'} + + chalk@4.1.0: + resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chunkd@2.0.1: + resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + ci-parallel-vars@1.0.1: + resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + cmd-shim@6.0.3: + resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + cmd-shim@7.0.0: + resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} + engines: {node: ^18.17.0 || >=20.5.0} + + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + columnify@1.6.0: + resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} + engines: {node: '>=8.0.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + concordance@5.0.4: + resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} + engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} + + concurrently@6.5.1: + resolution: {integrity: sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==} + engines: {node: '>=10.0.0'} + hasBin: true + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-core@5.0.1: + resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==} + engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. + + conventional-changelog-preset-loader@3.0.0: + resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==} + engines: {node: '>=14'} + + conventional-changelog-writer@6.0.1: + resolution: {integrity: sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ==} + engines: {node: '>=14'} + hasBin: true + + conventional-commits-filter@3.0.0: + resolution: {integrity: sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==} + engines: {node: '>=14'} + + conventional-commits-parser@4.0.0: + resolution: {integrity: sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==} + engines: {node: '>=14'} + hasBin: true + + conventional-recommended-bump@7.0.1: + resolution: {integrity: sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA==} + engines: {node: '>=14'} + hasBin: true + + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.4.4: + resolution: {integrity: sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==} + peerDependencies: + srvx: '>=0.7.1' + peerDependenciesMeta: + srvx: + optional: true + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + currently-unhandled@0.4.1: + resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} + engines: {node: '>=0.10.0'} + + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + date-time@3.1.0: + resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} + engines: {node: '>=6'} + + dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@5.0.0: + resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} + engines: {node: '>=4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + ejs@5.0.1: + resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} + engines: {node: '>=0.12.18'} + hasBin: true + + emittery@1.2.1: + resolution: {integrity: sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw==} + engines: {node: '>=14.16'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + envinfo@7.13.0: + resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} + engines: {node: '>=4'} + hasBin: true + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + execa@5.0.0: + resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-ws@5.0.2: + resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} + engines: {node: '>=4.5.0'} + peerDependencies: + express: ^4.0.0 || ^5.0.0-alpha.1 + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-xml-builder@1.3.0: + resolution: {integrity: sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==} + + fast-xml-parser@5.10.1: + resolution: {integrity: sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.0: + resolution: {integrity: sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==} + engines: {node: '>=10'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + git-raw-commits@3.0.0: + resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} + engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + + git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + + git-semver-tags@5.0.1: + resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==} + engines: {node: '>=14'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + + git-up@7.0.0: + resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} + + git-url-parse@14.0.0: + resolution: {integrity: sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==} + + gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.3: + resolution: {integrity: sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==} + engines: {node: 20 || >=22} + + globby@16.1.1: + resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} + engines: {node: '>=20'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.13.5: + resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==} + engines: {node: '>=16.9.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore-by-default@2.1.0: + resolution: {integrity: sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==} + engines: {node: '>=10 <11 || >=12 <13 || >=14'} + + ignore-walk@8.0.0: + resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} + engines: {node: ^20.17.0 || >=22.9.0} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + init-package-json@8.2.2: + resolution: {integrity: sha512-pXVMn67Jdw2hPKLCuJZj62NC9B2OIDd1R3JwZXTHXuEnfN3Uq5kJbKOSld6YEU+KOGfMD82EzxFTYz5o0SSJoA==} + engines: {node: ^20.17.0 || >=22.9.0} + + inquirer@12.9.6: + resolution: {integrity: sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + ioredis@5.6.1: + resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} + engines: {node: '>=12.22.0'} + + ip-address@10.4.0: + resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + irregular-plurals@4.2.0: + resolution: {integrity: sha512-bW9UXHL7bnUcNtTo+9ccSngbxc+V40H32IgvdVin0Xs8gbo+AVYD5g/72ce/54Kjfhq66vcZr8H8TKEvsifeOw==} + engines: {node: '>=18.20'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-ssh@1.4.1: + resolution: {integrity: sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==} + + is-stream@2.0.0: + resolution: {integrity: sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-unsafe@2.0.0: + resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} + hasBin: true + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-parse-even-better-errors@5.0.0: + resolution: {integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + json-stringify-nice@1.1.4: + resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + just-diff-apply@5.5.0: + resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} + + just-diff@6.0.2: + resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} + + just-extend@6.2.0: + resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + jwt-encode@1.0.1: + resolution: {integrity: sha512-QrGiNhynbAYyFdbC1GbjborzenSFs5Ga+2nE0uBokGXsH11xrgd1AX55HR7P+wGQyyZOn6LUO5iKlh74dlhBkA==} + + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa-easy-ws@1.3.1: + resolution: {integrity: sha512-LH6gIF3tR9K9K/gWUNDwvzNhuYYjSqAa07QmwOQlb0aNSMyHYFQn06luqseTUcxSCgCyE1HHEgzYhj0RFTJ3ag==} + + koa@3.1.2: + resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==} + engines: {node: '>= 18'} + + lerna@9.0.5: + resolution: {integrity: sha512-LtwZu2wINHlKpjRCxrEdK3QopyeUpFuUS4v6uzLYdg/uxnAKqDHrGY/mDPxdxDR3YAXJzpWXBdz49AHNIKZaSg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + hasBin: true + + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + + libnpmaccess@10.0.3: + resolution: {integrity: sha512-JPHTfWJxIK+NVPdNMNGnkz4XGX56iijPbe0qFWbdt68HL+kIvSzh+euBL8npLZvl2fpaxo+1eZSdoG15f5YdIQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + libnpmpublish@11.1.2: + resolution: {integrity: sha512-tNcU3cLH7toloAzhOOrBDhjzgbxpyuYvkf+BPPnnJCdc5EIcdJ8JcT+SglvCQKKyZ6m9dVXtCVlJcA6csxKdEA==} + engines: {node: ^20.17.0 || >=22.9.0} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + load-json-file@6.2.0: + resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} + engines: {node: '>=8'} + + load-json-file@7.0.1: + resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-fetch-happen@15.0.2: + resolution: {integrity: sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + make-fetch-happen@15.0.3: + resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} + engines: {node: ^20.17.0 || >=22.9.0} + + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} + hasBin: true + + matcher@6.0.0: + resolution: {integrity: sha512-TzDerdcNtI79w7Av4GT57bLdElPA/VAkjqdMZv8yhuc8geU2z0ljW9anXbX/55aHEMTpYypZb1lxsA/46r9oOQ==} + engines: {node: '>=20'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5-hex@3.0.1: + resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} + engines: {node: '>=8'} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memoize@10.2.0: + resolution: {integrity: sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==} + engines: {node: '>=18'} + + meow@14.0.0: + resolution: {integrity: sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==} + engines: {node: '>=20'} + + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.4: + resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@4.0.1: + resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + minipass-fetch@5.0.1: + resolution: {integrity: sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mockdate@3.0.5: + resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} + + modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multimatch@5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next@16.3.3: + resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + nise@5.1.9: + resolution: {integrity: sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + + nofilter@3.1.0: + resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} + engines: {node: '>=12.19'} + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-bundled@4.0.0: + resolution: {integrity: sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-bundled@5.0.0: + resolution: {integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-install-checks@7.1.2: + resolution: {integrity: sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-install-checks@8.0.0: + resolution: {integrity: sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-normalize-package-bin@5.0.0: + resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-package-arg@12.0.2: + resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@13.0.1: + resolution: {integrity: sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-packlist@10.0.3: + resolution: {integrity: sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-pick-manifest@10.0.0: + resolution: {integrity: sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-pick-manifest@11.0.3: + resolution: {integrity: sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-registry-fetch@19.1.0: + resolution: {integrity: sha512-xyZLfs7TxPu/WKjHUs0jZOPinzBAI32kEUel6za0vH+JUTnFZ5zbHI1ZoGZRDm6oMjADtrli6FxtMlk/5ABPNw==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nx@22.7.8: + resolution: {integrity: sha512-ceEhmaGCvY7oi7L7G/Nm/UZSnbfwaJxU1XbDLro7CTmVcnrmxAnxExCp0J/Tpf1xQaMZtwxgV7QATdKA/7vLQw==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.11.1 + '@swc/core': ^1.15.8 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} + engines: {node: '>=10'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map-series@2.1.0: + resolution: {integrity: sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + + p-pipe@3.1.0: + resolution: {integrity: sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw==} + engines: {node: '>=8'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-reduce@2.1.0: + resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + p-waterfall@2.1.1: + resolution: {integrity: sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw==} + engines: {node: '>=8'} + + package-config@5.0.0: + resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==} + engines: {node: '>=18'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pacote@21.0.1: + resolution: {integrity: sha512-LHGIUQUrcDIJUej53KJz1BPvUuHrItrR2yrnN0Kl9657cJ0ZT6QJHk9wWPBnQZhYT5KLyZWrk9jaYc2aKDu4yw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + pacote@21.3.1: + resolution: {integrity: sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-conflict-json@4.0.0: + resolution: {integrity: sha512-37CN2VtcuvKgHUs8+0b1uJeEsbGn61GRHz469C94P5xiOoqpDYJYwjg4RY9Vmz39WyZAVkR5++nbJwLMIgOCnQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-path@7.1.0: + resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} + + parse-url@8.1.0: + resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + plur@6.0.0: + resolution: {integrity: sha512-Y9wXQivjRX0REtwpA9+n0bYYypWESn3cWtW2vazymw711qn+AQXxzZjRqhANYGBLIMC1UzVdpwe/1hHQwHfwng==} + engines: {node: '>=20'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + proggy@3.0.0: + resolution: {integrity: sha512-QE8RApCM3IaRRxVzxrjbgNMpQEX6Wu0p0KBeoSiSEw5/bsGwZHsshF4LCxH2jp/r6BU+bqA3LrMDEYNfJnpD8Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + promise-all-reject-late@1.0.1: + resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} + + promise-call-limit@3.0.2: + resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + promzard@2.0.0: + resolution: {integrity: sha512-Ncd0vyS2eXGOjchIRg6PVCYKetJYrW1BSbbIo+bKdig61TB6nH2RQNF2uP+qMpsI73L/jURLWojcw8JNIKZ3gg==} + engines: {node: ^18.17.0 || >=20.5.0} + + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.11.0: + resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} + + prosemirror-view@1.41.6: + resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==} + + protocols@2.0.2: + resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + read-cmd-shim@4.0.0: + resolution: {integrity: sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + read-cmd-shim@5.0.0: + resolution: {integrity: sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==} + engines: {node: ^18.17.0 || >=20.5.0} + + read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read@4.1.0: + resolution: {integrity: sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA==} + engines: {node: ^18.17.0 || >=20.5.0} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redis@4.7.1: + resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@6.1.2: + resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + engines: {node: 20 || >=22} + hasBin: true + + rolldown-plugin-dts@0.22.3: + resolution: {integrity: sha512-APIGZGChvLVu05f+7bMmgf+lpvhjIvELhkOsg7c/95IVdOgULVFOX9iciaHJLaBfZeTthIgp+gryGBjgyKNA1A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-rc.3 + typescript: ^5.0.0 || ^6.0.0-beta + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-rc.6: + resolution: {integrity: sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sigstore@4.1.1: + resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} + engines: {node: ^20.17.0 || >=22.9.0} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sinon@12.0.1: + resolution: {integrity: sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==} + deprecated: 16.1.1 + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sort-keys@2.0.0: + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + ssri@12.0.0: + resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supertap@3.0.1: + resolution: {integrity: sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + temp-dir@1.0.0: + resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} + engines: {node: '>=4'} + + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + time-zone@1.0.0: + resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} + engines: {node: '>=4'} + + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + treeverse@3.0.0: + resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + ts.cryptojs256@1.0.1: + resolution: {integrity: sha512-9XtEgRVOZBCdpPcCEhfvv9I2AVXdvfI81I/KpFM0wEfbq5JVHlXH7bfIjGQmYrIHGiBEHKsIaStQ87k926J7dA==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + tuf-js@4.1.0: + resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + + type-fest@0.4.1: + resolution: {integrity: sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==} + engines: {node: '>=6'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + unique-filename@5.0.0: + resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} + engines: {node: ^20.17.0 || >=22.9.0} + + unique-slug@6.0.0: + resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==} + engines: {node: ^20.17.0 || >=22.9.0} + + universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@6.0.2: + resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + well-known-symbols@2.0.0: + resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} + engines: {node: '>=6'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + write-file-atomic@6.0.0: + resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} + engines: {node: ^20.17.0 || >=22.9.0} + + write-json-file@3.2.0: + resolution: {integrity: sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==} + engines: {node: '>=6'} + + write-pkg@4.0.0: + resolution: {integrity: sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA==} + engines: {node: '>=8'} + + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y-prosemirror@1.3.7: + resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 + + y-protocols@1.0.7: + resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ava/typescript@3.0.1': + dependencies: + escape-string-regexp: 5.0.0 + execa: 5.1.1 + optional: true + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.1 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.990.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.10 + '@aws-sdk/credential-provider-node': 3.972.9 + '@aws-sdk/middleware-bucket-endpoint': 3.972.3 + '@aws-sdk/middleware-expect-continue': 3.972.3 + '@aws-sdk/middleware-flexible-checksums': 3.972.8 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-location-constraint': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-sdk-s3': 3.972.10 + '@aws-sdk/middleware-ssec': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.10 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/signature-v4-multi-region': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.990.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.8 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.23.0 + '@smithy/eventstream-serde-browser': 4.2.8 + '@smithy/eventstream-serde-config-resolver': 4.3.8 + '@smithy/eventstream-serde-node': 4.2.8 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-blob-browser': 4.2.9 + '@smithy/hash-node': 4.2.8 + '@smithy/hash-stream-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/md5-js': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.10 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-stream': 4.5.12 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.8 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.990.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.10 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.10 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.990.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.8 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.23.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.10 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.10': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/xml-builder': 3.972.4 + '@smithy/core': 3.23.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.8': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.10': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/types': 3.973.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.10 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.12 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.8': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/credential-provider-env': 3.972.8 + '@aws-sdk/credential-provider-http': 3.972.10 + '@aws-sdk/credential-provider-login': 3.972.8 + '@aws-sdk/credential-provider-process': 3.972.8 + '@aws-sdk/credential-provider-sso': 3.972.8 + '@aws-sdk/credential-provider-web-identity': 3.972.8 + '@aws-sdk/nested-clients': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.8': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/nested-clients': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.9': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.8 + '@aws-sdk/credential-provider-http': 3.972.10 + '@aws-sdk/credential-provider-ini': 3.972.8 + '@aws-sdk/credential-provider-process': 3.972.8 + '@aws-sdk/credential-provider-sso': 3.972.8 + '@aws-sdk/credential-provider-web-identity': 3.972.8 + '@aws-sdk/types': 3.973.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.8': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.8': + dependencies: + '@aws-sdk/client-sso': 3.990.0 + '@aws-sdk/core': 3.973.10 + '@aws-sdk/token-providers': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.8': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/nested-clients': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.2 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.972.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.10 + '@aws-sdk/crc64-nvme': 3.972.0 + '@aws-sdk/types': 3.973.1 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.12 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.10': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-arn-parser': 3.972.2 + '@smithy/core': 3.23.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.12 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.10': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.990.0 + '@smithy/core': 3.23.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.990.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.10 + '@aws-sdk/middleware-host-header': 3.972.3 + '@aws-sdk/middleware-logger': 3.972.3 + '@aws-sdk/middleware-recursion-detection': 3.972.3 + '@aws-sdk/middleware-user-agent': 3.972.10 + '@aws-sdk/region-config-resolver': 3.972.3 + '@aws-sdk/types': 3.973.1 + '@aws-sdk/util-endpoints': 3.990.0 + '@aws-sdk/util-user-agent-browser': 3.972.3 + '@aws-sdk/util-user-agent-node': 3.972.8 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.23.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-retry': 4.4.31 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.10 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.30 + '@smithy/util-defaults-mode-node': 4.2.33 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.990.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.10 + '@aws-sdk/types': 3.973.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.990.0': + dependencies: + '@aws-sdk/core': 3.973.10 + '@aws-sdk/nested-clients': 3.990.0 + '@aws-sdk/types': 3.973.1 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.1': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.2': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.990.0': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.4': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.3': + dependencies: + '@aws-sdk/types': 3.973.1 + '@smithy/types': 4.12.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.972.8': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.10 + '@aws-sdk/types': 3.973.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.4': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.10.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.3': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@8.0.0-rc.2': + dependencies: + '@babel/parser': 8.0.0-rc.2 + '@babel/types': 8.0.0-rc.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-string-parser@8.0.0-rc.2': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-identifier@8.0.0-rc.2': {} + + '@babel/parser@8.0.0-rc.2': + dependencies: + '@babel/types': 8.0.0-rc.2 + + '@babel/runtime@7.28.6': {} + + '@babel/types@8.0.0-rc.2': + dependencies: + '@babel/helper-string-parser': 8.0.0-rc.2 + '@babel/helper-validator-identifier': 8.0.0-rc.2 + + '@biomejs/biome@2.4.5': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.5 + '@biomejs/cli-darwin-x64': 2.4.5 + '@biomejs/cli-linux-arm64': 2.4.5 + '@biomejs/cli-linux-arm64-musl': 2.4.5 + '@biomejs/cli-linux-x64': 2.4.5 + '@biomejs/cli-linux-x64-musl': 2.4.5 + '@biomejs/cli-win32-arm64': 2.4.5 + '@biomejs/cli-win32-x64': 2.4.5 + + '@biomejs/cli-darwin-arm64@2.4.5': + optional: true + + '@biomejs/cli-darwin-x64@2.4.5': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.5': + optional: true + + '@biomejs/cli-linux-arm64@2.4.5': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.5': + optional: true + + '@biomejs/cli-linux-x64@2.4.5': + optional: true + + '@biomejs/cli-win32-arm64@2.4.5': + optional: true + + '@biomejs/cli-win32-x64@2.4.5': + optional: true + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@gar/promise-retry@1.0.3': {} + + '@hono/node-server@2.0.12(hono@4.13.5)': + dependencies: + hono: 4.13.5 + + '@hono/node-ws@1.3.0(@hono/node-server@2.0.12(hono@4.13.5))(hono@4.13.5)': + dependencies: + '@hono/node-server': 2.0.12(hono@4.13.5) + hono: 4.13.5 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@hutson/parse-repository-url@3.0.2': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.3 + optional: true + + '@img/sharp-darwin-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.3 + optional: true + + '@img/sharp-linux-arm@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.3 + optional: true + + '@img/sharp-linux-ppc64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.3 + optional: true + + '@img/sharp-linux-riscv64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.3 + optional: true + + '@img/sharp-linux-s390x@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.3 + optional: true + + '@img/sharp-linux-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + optional: true + + '@img/sharp-wasm32@0.35.4': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-win32-arm64@0.35.4': + optional: true + + '@img/sharp-win32-ia32@0.35.4': + optional: true + + '@img/sharp-win32-x64@0.35.4': + optional: true + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.19.11)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.11) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/confirm@5.1.21(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/core@10.3.2(@types/node@22.19.11)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.11) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/editor@4.2.23(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/expand@4.0.23(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.11)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/number@3.0.23(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/password@4.0.23(@types/node@22.19.11)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/prompts@7.10.1(@types/node@22.19.11)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.11) + '@inquirer/confirm': 5.1.21(@types/node@22.19.11) + '@inquirer/editor': 4.2.23(@types/node@22.19.11) + '@inquirer/expand': 4.0.23(@types/node@22.19.11) + '@inquirer/input': 4.3.1(@types/node@22.19.11) + '@inquirer/number': 3.0.23(@types/node@22.19.11) + '@inquirer/password': 4.0.23(@types/node@22.19.11) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.11) + '@inquirer/search': 3.2.2(@types/node@22.19.11) + '@inquirer/select': 4.4.2(@types/node@22.19.11) + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/rawlist@4.1.11(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/search@3.2.2(@types/node@22.19.11)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.11) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/select@4.4.2(@types/node@22.19.11)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.11) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.11 + + '@inquirer/type@3.0.10(@types/node@22.19.11)': + optionalDependencies: + '@types/node': 22.19.11 + + '@ioredis/commands@1.5.0': {} + + '@isaacs/cliui@9.0.0': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@isaacs/string-locale-compare@1.1.0': {} + + '@jest/diff-sequences@30.0.1': {} + + '@jest/get-type@30.1.0': {} + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lerna/create@9.0.5(@types/node@22.19.11)(supports-color@7.2.0)(typescript@5.8.2)': + dependencies: + '@npmcli/arborist': 9.1.6(supports-color@7.2.0) + '@npmcli/package-json': 7.0.2 + '@npmcli/run-script': 10.0.3(supports-color@7.2.0) + '@nx/devkit': 22.5.1(nx@22.7.8) + '@octokit/plugin-enterprise-rest': 6.0.1 + '@octokit/rest': 20.1.2 + aproba: 2.0.0 + byte-size: 8.1.1 + chalk: 4.1.0 + cmd-shim: 6.0.3 + color-support: 1.1.3 + columnify: 1.6.0 + console-control-strings: 1.1.0 + conventional-changelog-core: 5.0.1 + conventional-recommended-bump: 7.0.1 + cosmiconfig: 9.0.0(typescript@5.8.2) + dedent: 1.5.3 + execa: 5.0.0 + fs-extra: 11.3.3 + get-stream: 6.0.0 + git-url-parse: 14.0.0 + glob-parent: 6.0.2 + has-unicode: 2.0.1 + ini: 1.3.8 + init-package-json: 8.2.2 + inquirer: 12.9.6(@types/node@22.19.11) + is-ci: 3.0.1 + is-stream: 2.0.0 + js-yaml: 4.3.1 + libnpmpublish: 11.1.2(supports-color@7.2.0) + load-json-file: 6.2.0 + make-dir: 4.0.0 + make-fetch-happen: 15.0.2(supports-color@7.2.0) + minimatch: 3.1.4 + multimatch: 5.0.0 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + nx: 22.7.8 + p-map: 4.0.0 + p-map-series: 2.1.0 + p-queue: 6.6.2 + p-reduce: 2.1.0 + pacote: 21.0.1(supports-color@7.2.0) + pify: 5.0.0 + read-cmd-shim: 4.0.0 + resolve-from: 5.0.0 + rimraf: 6.1.2 + semver: 7.7.2 + set-blocking: 2.0.0 + signal-exit: 3.0.7 + slash: 3.0.0 + ssri: 12.0.0 + string-width: 4.2.3 + tar: 7.5.22 + temp-dir: 1.0.0 + through: 2.3.8 + tinyglobby: 0.2.12 + upath: 2.0.1 + uuid: 11.1.1 + validate-npm-package-license: 3.0.4 + validate-npm-package-name: 6.0.2 + wide-align: 1.1.5 + write-file-atomic: 5.0.1 + write-pkg: 4.0.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - '@types/node' + - babel-plugin-macros + - supports-color + - typescript + + '@lifeomic/attempt@3.1.0': {} + + '@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)(supports-color@5.5.0)': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6(supports-color@5.5.0) + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 8.1.0 + semver: 7.8.5 + tar: 7.5.22 + transitivePeerDependencies: + - encoding + - supports-color + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.9.0 + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.3.3': {} + + '@next/swc-darwin-arm64@16.3.3': + optional: true + + '@next/swc-darwin-x64@16.3.3': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.3': + optional: true + + '@next/swc-linux-arm64-musl@16.3.3': + optional: true + + '@next/swc-linux-x64-gnu@16.3.3': + optional: true + + '@next/swc-linux-x64-musl@16.3.3': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.3': + optional: true + + '@next/swc-win32-x64-msvc@16.3.3': + optional: true + + '@nodable/entities@3.0.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@npmcli/agent@4.0.0(supports-color@7.2.0)': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) + lru-cache: 11.2.6 + socks-proxy-agent: 8.0.5(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@npmcli/arborist@9.1.6(supports-color@7.2.0)': + dependencies: + '@isaacs/string-locale-compare': 1.1.0 + '@npmcli/fs': 4.0.0 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/map-workspaces': 5.0.3 + '@npmcli/metavuln-calculator': 9.0.3(supports-color@7.2.0) + '@npmcli/name-from-folder': 3.0.0 + '@npmcli/node-gyp': 4.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/query': 4.0.1 + '@npmcli/redact': 3.2.2 + '@npmcli/run-script': 10.0.3(supports-color@7.2.0) + bin-links: 5.0.0 + cacache: 20.0.3 + common-ancestor-path: 1.0.1 + hosted-git-info: 9.0.2 + json-stringify-nice: 1.1.4 + lru-cache: 11.2.6 + minimatch: 10.2.4 + nopt: 8.1.0 + npm-install-checks: 7.1.2 + npm-package-arg: 13.0.1 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + pacote: 21.3.1(supports-color@7.2.0) + parse-conflict-json: 4.0.0 + proc-log: 5.0.0 + proggy: 3.0.0 + promise-all-reject-late: 1.0.1 + promise-call-limit: 3.0.2 + semver: 7.7.4 + ssri: 12.0.0 + treeverse: 3.0.0 + walk-up-path: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@4.0.0': + dependencies: + semver: 7.8.5 + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.8.5 + + '@npmcli/git@6.0.3': + dependencies: + '@npmcli/promise-spawn': 8.0.3 + ini: 5.0.0 + lru-cache: 10.4.3 + npm-pick-manifest: 10.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + semver: 7.8.5 + which: 5.0.0 + + '@npmcli/git@7.0.1': + dependencies: + '@npmcli/promise-spawn': 9.0.1 + ini: 6.0.0 + lru-cache: 11.2.6 + npm-pick-manifest: 11.0.3 + proc-log: 6.1.0 + promise-retry: 2.0.1 + semver: 7.8.5 + which: 6.0.1 + + '@npmcli/installed-package-contents@3.0.0': + dependencies: + npm-bundled: 4.0.0 + npm-normalize-package-bin: 4.0.0 + + '@npmcli/installed-package-contents@4.0.0': + dependencies: + npm-bundled: 5.0.0 + npm-normalize-package-bin: 5.0.0 + + '@npmcli/map-workspaces@5.0.3': + dependencies: + '@npmcli/name-from-folder': 4.0.0 + '@npmcli/package-json': 7.0.2 + glob: 13.0.3 + minimatch: 10.2.5 + + '@npmcli/metavuln-calculator@9.0.3(supports-color@7.2.0)': + dependencies: + cacache: 20.0.3 + json-parse-even-better-errors: 5.0.0 + pacote: 21.3.1(supports-color@7.2.0) + proc-log: 6.1.0 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/name-from-folder@3.0.0': {} + + '@npmcli/name-from-folder@4.0.0': {} + + '@npmcli/node-gyp@4.0.0': {} + + '@npmcli/node-gyp@5.0.0': {} + + '@npmcli/package-json@7.0.2': + dependencies: + '@npmcli/git': 7.0.1 + glob: 11.1.0 + hosted-git-info: 9.0.2 + json-parse-even-better-errors: 5.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + + '@npmcli/promise-spawn@8.0.3': + dependencies: + which: 5.0.0 + + '@npmcli/promise-spawn@9.0.1': + dependencies: + which: 6.0.1 + + '@npmcli/query@4.0.1': + dependencies: + postcss-selector-parser: 7.1.1 + + '@npmcli/redact@3.2.2': {} + + '@npmcli/redact@4.0.0': {} + + '@npmcli/run-script@10.0.3(supports-color@7.2.0)': + dependencies: + '@npmcli/node-gyp': 5.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/promise-spawn': 9.0.1 + node-gyp: 12.2.0(supports-color@7.2.0) + proc-log: 6.1.0 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + + '@nx/devkit@22.5.1(nx@22.7.8)': + dependencies: + '@zkochan/js-yaml': 0.0.7 + ejs: 3.1.10 + enquirer: 2.3.6 + minimatch: 10.2.5 + nx: 22.7.8 + semver: 7.7.4 + tslib: 2.8.1 + yargs-parser: 21.1.1 + + '@nx/nx-darwin-arm64@22.7.8': + optional: true + + '@nx/nx-darwin-x64@22.7.8': + optional: true + + '@nx/nx-freebsd-x64@22.7.8': + optional: true + + '@nx/nx-linux-arm-gnueabihf@22.7.8': + optional: true + + '@nx/nx-linux-arm64-gnu@22.7.8': + optional: true + + '@nx/nx-linux-arm64-musl@22.7.8': + optional: true + + '@nx/nx-linux-x64-gnu@22.7.8': + optional: true + + '@nx/nx-linux-x64-musl@22.7.8': + optional: true + + '@nx/nx-win32-arm64-msvc@22.7.8': + optional: true + + '@nx/nx-win32-x64-msvc@22.7.8': + optional: true + + '@octokit/auth-token@4.0.0': {} + + '@octokit/core@5.2.2': + dependencies: + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.1 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + + '@octokit/endpoint@9.0.6': + dependencies: + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + + '@octokit/graphql@7.1.1': + dependencies: + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/plugin-enterprise-rest@6.0.1': {} + + '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)': + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)': + dependencies: + '@octokit/core': 5.2.2 + + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)': + dependencies: + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 + + '@octokit/request-error@5.1.1': + dependencies: + '@octokit/types': 13.10.0 + deprecation: 2.3.1 + once: 1.4.0 + + '@octokit/request@8.4.1': + dependencies: + '@octokit/endpoint': 9.0.6 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 + universal-user-agent: 6.0.1 + + '@octokit/rest@20.1.2': + dependencies: + '@octokit/core': 5.2.2 + '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@oxc-project/types@0.115.0': {} + + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@remirror/core-constants@3.0.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.6': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.6': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.6': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.6': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.6': {} + + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + + '@sesamecare-oss/redlock@1.4.0(ioredis@5.6.1(supports-color@5.5.0))': + dependencies: + ioredis: 5.6.1(supports-color@5.5.0) + + '@sigstore/bundle@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.0 + + '@sigstore/core@3.2.1': {} + + '@sigstore/protobuf-specs@0.5.0': {} + + '@sigstore/sign@4.1.1(supports-color@7.2.0)': + dependencies: + '@gar/promise-retry': 1.0.3 + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.0 + make-fetch-happen: 15.0.6(supports-color@7.2.0) + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@4.0.2(supports-color@7.2.0)': + dependencies: + '@sigstore/protobuf-specs': 0.5.0 + tuf-js: 4.1.0(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + '@sigstore/verify@3.1.1': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.0 + + '@sinclair/typebox@0.34.48': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sinonjs/commons@1.8.6': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@11.3.1': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@8.1.0': + dependencies: + '@sinonjs/commons': 1.8.6 + + '@sinonjs/samsam@6.1.3': + dependencies: + '@sinonjs/commons': 1.8.6 + lodash.get: 4.4.2 + type-detect: 4.1.0 + + '@sinonjs/text-encoding@0.7.3': {} + + '@smithy/abort-controller@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.1': + dependencies: + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.6': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/core@3.23.0': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.12 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.8': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.8': + dependencies: + '@smithy/eventstream-codec': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.9': + dependencies: + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.14': + dependencies: + '@smithy/core': 3.23.0 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.31': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.8': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.4.10': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + + '@smithy/shared-ini-file-loader@4.4.3': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.8': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.11.3': + dependencies: + '@smithy/core': 3.23.0 + '@smithy/middleware-endpoint': 4.4.14 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.12 + tslib: 2.8.1 + + '@smithy/types@4.12.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.8': + dependencies: + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.30': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.33': + dependencies: + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.11.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.8': + dependencies: + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.12': + dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.10 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.8': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.25 + tailwindcss: 4.1.18 + + '@tiptap/core@3.19.0(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/pm': 3.19.0 + + '@tiptap/core@3.30.5(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/pm': 3.19.0 + + '@tiptap/extension-blockquote@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-bold@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-bubble-menu@3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + optional: true + + '@tiptap/extension-bullet-list@3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extension-list': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-code-block@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/extension-code@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-collaboration-caret@3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))': + dependencies: + '@tiptap/core': 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + '@tiptap/y-tiptap': 3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + + '@tiptap/extension-collaboration@3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + '@tiptap/core': 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + '@tiptap/y-tiptap': 3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 + + '@tiptap/extension-document@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-dropcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extensions': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-floating-menu@3.19.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@floating-ui/dom': 1.7.5 + '@tiptap/core': 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/extension-gapcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extensions': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-hard-break@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-heading@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-horizontal-rule@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/extension-italic@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-link@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extension-list': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-list-keymap@3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extension-list': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/extension-ordered-list@3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/extension-list': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + + '@tiptap/extension-paragraph@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-strike@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-text@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extension-underline@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + + '@tiptap/extensions@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/pm@3.19.0': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + '@tiptap/react@3.19.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tiptap/core': 3.30.5(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.19.0(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-floating-menu': 3.19.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.30.5(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.19.0': + dependencies: + '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) + '@tiptap/extension-blockquote': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-bold': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-bullet-list': 3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-code': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-code-block': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-document': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-dropcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-gapcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-hard-break': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-heading': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-horizontal-rule': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-italic': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-link': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-list': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-list-item': 3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-list-keymap': 3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-ordered-list': 3.19.0(@tiptap/extension-list@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)) + '@tiptap/extension-paragraph': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-strike': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-text': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extension-underline': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) + '@tiptap/extensions': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/pm': 3.19.0 + + '@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + + '@tufjs/canonical-json@2.0.0': {} + + '@tufjs/models@4.1.0': + dependencies: + '@tufjs/canonical-json': 2.0.0 + minimatch: 10.2.5 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@types/accepts@1.3.7': + dependencies: + '@types/node': 16.18.126 + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 22.19.11 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 16.18.126 + + '@types/bun@1.3.9': + dependencies: + bun-types: 1.3.9 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 16.18.126 + + '@types/content-disposition@0.5.9': {} + + '@types/cookies@0.9.2': + dependencies: + '@types/connect': 3.4.38 + '@types/express': 4.17.25 + '@types/keygrip': 1.0.6 + '@types/node': 16.18.126 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 16.18.126 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 16.18.126 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-ws@3.0.6': + dependencies: + '@types/express': 4.17.25 + '@types/express-serve-static-core': 5.1.1 + '@types/ws': 8.18.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-assert@1.5.6': {} + + '@types/http-errors@2.0.5': {} + + '@types/jsesc@2.5.1': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 16.18.126 + + '@types/keygrip@1.0.6': {} + + '@types/koa-compose@3.2.9': + dependencies: + '@types/koa': 2.15.0 + + '@types/koa@2.15.0': + dependencies: + '@types/accepts': 1.3.7 + '@types/content-disposition': 0.5.9 + '@types/cookies': 0.9.2 + '@types/http-assert': 1.5.6 + '@types/http-errors': 2.0.5 + '@types/keygrip': 1.0.6 + '@types/koa-compose': 3.2.9 + '@types/node': 16.18.126 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/mime@1.3.5': {} + + '@types/minimatch@3.0.5': {} + + '@types/minimist@1.2.5': {} + + '@types/ms@2.1.0': {} + + '@types/node@16.18.126': {} + + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/redis@4.0.11': + dependencies: + redis: 4.7.1 + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 16.18.126 + + '@types/send@1.2.1': + dependencies: + '@types/node': 16.18.126 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 16.18.126 + '@types/send': 0.17.6 + + '@types/sinon@10.0.20': + dependencies: + '@types/sinonjs__fake-timers': 15.0.1 + + '@types/sinonjs__fake-timers@15.0.1': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.11 + + '@vercel/nft@1.3.2(encoding@0.1.13)(supports-color@5.5.0)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)(supports-color@5.5.0) + '@rollup/pluginutils': 5.3.0 + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.3 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@yarnpkg/lockfile@1.1.0': {} + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abbrev@3.0.1: {} + + abbrev@4.0.0: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + add-stream@1.0.0: {} + + agent-base@6.0.2(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + anynum@1.0.1: {} + + aproba@2.0.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-differ@3.0.0: {} + + array-find-index@1.0.2: {} + + array-flatten@1.1.1: {} + + array-ify@1.0.0: {} + + array-union@2.1.0: {} + + arrgv@1.0.2: {} + + arrify@1.0.1: {} + + arrify@2.0.1: {} + + arrify@3.0.0: {} + + ast-kit@3.0.0-beta.1: + dependencies: + '@babel/parser': 8.0.0-rc.2 + estree-walker: 3.0.3 + pathe: 2.0.3 + + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + + async-sema@3.1.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + ava@7.0.0(@ava/typescript@3.0.1)(encoding@0.1.13)(supports-color@5.5.0): + dependencies: + '@vercel/nft': 1.3.2(encoding@0.1.13)(supports-color@5.5.0) + acorn: 8.16.0 + acorn-walk: 8.3.5 + ansi-styles: 6.2.3 + arrgv: 1.0.2 + arrify: 3.0.0 + callsites: 4.2.0 + cbor: 10.0.11 + chalk: 5.6.2 + chunkd: 2.0.1 + ci-info: 4.4.0 + ci-parallel-vars: 1.0.1 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + common-path-prefix: 3.0.0 + concordance: 5.0.4 + currently-unhandled: 0.4.1 + debug: 4.4.3(supports-color@5.5.0) + emittery: 1.2.1 + figures: 6.1.0 + globby: 16.1.1 + ignore-by-default: 2.1.0 + indent-string: 5.0.0 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + matcher: 6.0.0 + memoize: 10.2.0 + ms: 2.1.3 + p-map: 7.0.4 + package-config: 5.0.0 + picomatch: 4.0.4 + plur: 6.0.0 + pretty-ms: 9.3.0 + resolve-cwd: 3.0.0 + stack-utils: 2.0.6 + supertap: 3.0.1 + temp-dir: 3.0.0 + write-file-atomic: 7.0.1 + yargs: 18.0.0 + optionalDependencies: + '@ava/typescript': 3.0.1 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.3: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.21: {} + + before-after-hook@2.2.3: {} + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bin-links@5.0.0: + dependencies: + cmd-shim: 7.0.0 + npm-normalize-package-bin: 4.0.0 + proc-log: 5.0.0 + read-cmd-shim: 5.0.0 + write-file-atomic: 6.0.0 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + birpc@4.0.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blueimp-md5@2.19.0: {} + + body-parser@1.20.6(supports-color@5.5.0): + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@5.5.0) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bowser@2.14.1: {} + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.3 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bun-types@1.3.9: + dependencies: + '@types/node': 22.19.11 + + byte-size@8.1.1: {} + + bytes@3.1.2: {} + + cacache@20.0.3: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.3 + lru-cache: 11.2.6 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + unique-filename: 5.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + callsites@4.2.0: {} + + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001810: {} + + cbor@10.0.11: + dependencies: + nofilter: 3.1.0 + + chalk@4.1.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chardet@2.1.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@3.0.0: {} + + chunkd@2.0.1: {} + + ci-info@3.9.0: {} + + ci-info@4.4.0: {} + + ci-parallel-vars@1.0.1: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.6.1: {} + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + clone@1.0.4: {} + + cluster-key-slot@1.1.2: {} + + cmd-shim@6.0.3: {} + + cmd-shim@7.0.0: {} + + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + columnify@1.6.0: + dependencies: + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + common-ancestor-path@1.0.1: {} + + common-path-prefix@3.0.0: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + concordance@5.0.4: + dependencies: + date-time: 3.1.0 + esutils: 2.0.3 + fast-diff: 1.3.0 + js-string-escape: 1.0.1 + lodash: 4.18.1 + md5-hex: 3.0.1 + semver: 7.7.4 + well-known-symbols: 2.0.0 + + concurrently@6.5.1: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.18.1 + rxjs: 6.6.7 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 16.2.0 + + consola@3.4.2: {} + + console-control-strings@1.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-core@5.0.1: + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 6.0.1 + conventional-commits-parser: 4.0.0 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 3.0.0 + git-remote-origin-url: 2.0.0 + git-semver-tags: 5.0.1 + normalize-package-data: 3.0.3 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + + conventional-changelog-preset-loader@3.0.0: {} + + conventional-changelog-writer@6.0.1: + dependencies: + conventional-commits-filter: 3.0.0 + dateformat: 3.0.3 + handlebars: 4.7.9 + json-stringify-safe: 5.0.1 + meow: 8.1.2 + semver: 7.8.5 + split: 1.0.1 + + conventional-commits-filter@3.0.0: + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + + conventional-commits-parser@4.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + meow: 8.1.2 + split2: 3.2.2 + + conventional-recommended-bump@7.0.1: + dependencies: + concat-stream: 2.0.0 + conventional-changelog-preset-loader: 3.0.0 + conventional-commits-filter: 3.0.0 + conventional-commits-parser: 4.0.0 + git-raw-commits: 3.0.0 + git-semver-tags: 5.0.1 + meow: 8.1.2 + + convert-to-spaces@2.0.1: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.0(typescript@5.8.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.8.2 + + crelt@1.0.6: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.4.4: {} + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + currently-unhandled@0.4.1: + dependencies: + array-find-index: 1.0.2 + + dargs@7.0.0: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.28.6 + + date-time@3.1.0: + dependencies: + time-zone: 1.0.0 + + dateformat@3.0.3: {} + + debug@2.6.9(supports-color@5.5.0): + dependencies: + ms: 2.0.0 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + dedent@1.5.3: {} + + deep-equal@1.0.1: {} + + deep-extend@0.6.0: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + denque@2.1.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + deprecation@2.3.1: {} + + destroy@1.2.0: {} + + detect-indent@5.0.0: {} + + detect-libc@2.1.2: {} + + diff@5.2.2: {} + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + dts-resolver@2.1.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + ejs@5.0.1: {} + + emittery@1.2.1: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + envinfo@7.13.0: {} + + err-code@2.0.3: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + execa@5.0.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + optional: true + + expand-template@2.0.3: {} + + exponential-backoff@3.1.3: {} + + express-ws@5.0.2(express@4.22.1(supports-color@5.5.0)): + dependencies: + express: 4.22.1(supports-color@5.5.0) + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + express@4.22.1(supports-color@5.5.0): + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.6(supports-color@5.5.0) + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2(supports-color@5.5.0) + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2(supports-color@5.5.0) + serve-static: 1.16.3(supports-color@5.5.0) + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-diff@1.3.0: {} + + fast-equals@5.4.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-xml-builder@1.3.0: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.10.1: + dependencies: + '@nodable/entities': 3.0.0 + fast-xml-builder: 1.3.0 + is-unsafe: 2.0.0 + path-expression-matcher: 1.6.2 + strnum: 2.4.1 + xml-naming: 0.3.0 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-uri-to-path@1.0.0: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.9 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2(supports-color@5.5.0): + dependencies: + debug: 2.6.9(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up-simple@1.0.1: {} + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-constants@1.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generic-pool@3.9.0: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-pkg-repo@4.2.1: + dependencies: + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.1.0 + through2: 2.0.5 + yargs: 16.2.0 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.0: {} + + get-stream@6.0.1: {} + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@3.0.0: + dependencies: + dargs: 7.0.0 + meow: 8.1.2 + split2: 3.2.2 + + git-remote-origin-url@2.0.0: + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + + git-semver-tags@5.0.1: + dependencies: + meow: 8.1.2 + semver: 7.8.5 + + git-up@7.0.0: + dependencies: + is-ssh: 1.4.1 + parse-url: 8.1.0 + + git-url-parse@14.0.0: + dependencies: + git-up: 7.0.0 + + gitconfiglocal@1.0.0: + dependencies: + ini: 1.3.8 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + + glob@13.0.3: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.2 + path-scurry: 2.0.1 + + globby@16.1.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + hard-rejection@2.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.13.5: {} + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.6 + + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + + http-cache-semantics@4.2.0: {} + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1(supports-color@7.2.0): + dependencies: + agent-base: 6.0.2(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@5.5.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore-by-default@2.1.0: {} + + ignore-walk@8.0.0: + dependencies: + minimatch: 10.2.4 + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.1.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + indent-string@5.0.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@5.0.0: {} + + ini@6.0.0: {} + + init-package-json@8.2.2: + dependencies: + '@npmcli/package-json': 7.0.2 + npm-package-arg: 13.0.1 + promzard: 2.0.0 + read: 4.1.0 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + validate-npm-package-name: 6.0.2 + + inquirer@12.9.6(@types/node@22.19.11): + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.11) + '@inquirer/prompts': 7.10.1(@types/node@22.19.11) + '@inquirer/type': 3.0.10(@types/node@22.19.11) + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.19.11 + + ioredis@5.6.1(supports-color@5.5.0): + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3(supports-color@5.5.0) + denque: 2.1.0 + 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 + transitivePeerDependencies: + - supports-color + + ip-address@10.4.0: {} + + ipaddr.js@1.9.1: {} + + irregular-plurals@4.2.0: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.4 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-path-inside@4.0.0: {} + + is-plain-obj@1.1.0: {} + + is-plain-object@5.0.0: {} + + is-promise@4.0.0: {} + + is-ssh@1.4.1: + dependencies: + protocols: 2.0.2 + + is-stream@2.0.0: {} + + is-stream@2.0.1: {} + + is-text-path@1.0.1: + dependencies: + text-extensions: 1.9.0 + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-unsafe@2.0.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + isexe@4.0.0: {} + + isomorphic.js@0.2.5: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + jest-diff@30.2.0: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.2.0 + + jiti@2.6.1: {} + + js-string-escape@1.0.1: {} + + js-tokens@4.0.0: {} + + js-yaml@3.15.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-parse-even-better-errors@5.0.0: {} + + json-stringify-nice@1.1.4: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + just-diff-apply@5.5.0: {} + + just-diff@6.0.2: {} + + just-extend@6.2.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + jwt-encode@1.0.1: + dependencies: + ts.cryptojs256: 1.0.1 + + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + kind-of@6.0.3: {} + + kleur@4.1.5: {} + + koa-compose@4.1.0: {} + + koa-easy-ws@1.3.1(supports-color@5.5.0): + dependencies: + debug: 4.4.3(supports-color@5.5.0) + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + koa@3.1.2: + dependencies: + accepts: 1.3.8 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookies: 0.9.1 + delegates: 1.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 2.0.1 + koa-compose: 4.1.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + + lerna@9.0.5(@types/node@22.19.11)(supports-color@7.2.0): + dependencies: + '@lerna/create': 9.0.5(@types/node@22.19.11)(supports-color@7.2.0)(typescript@5.8.2) + '@npmcli/arborist': 9.1.6(supports-color@7.2.0) + '@npmcli/package-json': 7.0.2 + '@npmcli/run-script': 10.0.3(supports-color@7.2.0) + '@nx/devkit': 22.5.1(nx@22.7.8) + '@octokit/plugin-enterprise-rest': 6.0.1 + '@octokit/rest': 20.1.2 + aproba: 2.0.0 + byte-size: 8.1.1 + chalk: 4.1.0 + cmd-shim: 6.0.3 + color-support: 1.1.3 + columnify: 1.6.0 + console-control-strings: 1.1.0 + conventional-changelog-angular: 7.0.0 + conventional-changelog-core: 5.0.1 + conventional-recommended-bump: 7.0.1 + cosmiconfig: 9.0.0(typescript@5.8.2) + dedent: 1.5.3 + envinfo: 7.13.0 + execa: 5.0.0 + fs-extra: 11.3.3 + get-port: 5.1.1 + get-stream: 6.0.0 + git-url-parse: 14.0.0 + glob-parent: 6.0.2 + has-unicode: 2.0.1 + import-local: 3.1.0 + ini: 1.3.8 + init-package-json: 8.2.2 + inquirer: 12.9.6(@types/node@22.19.11) + is-ci: 3.0.1 + is-stream: 2.0.0 + jest-diff: 30.2.0 + js-yaml: 4.3.1 + libnpmaccess: 10.0.3(supports-color@7.2.0) + libnpmpublish: 11.1.2(supports-color@7.2.0) + load-json-file: 6.2.0 + make-dir: 4.0.0 + make-fetch-happen: 15.0.2(supports-color@7.2.0) + minimatch: 3.1.4 + multimatch: 5.0.0 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + nx: 22.7.8 + p-map: 4.0.0 + p-map-series: 2.1.0 + p-pipe: 3.1.0 + p-queue: 6.6.2 + p-reduce: 2.1.0 + p-waterfall: 2.1.1 + pacote: 21.0.1(supports-color@7.2.0) + pify: 5.0.0 + read-cmd-shim: 4.0.0 + resolve-from: 5.0.0 + rimraf: 6.1.2 + semver: 7.7.2 + set-blocking: 2.0.0 + signal-exit: 3.0.7 + slash: 3.0.0 + ssri: 12.0.0 + string-width: 4.2.3 + tar: 7.5.22 + temp-dir: 1.0.0 + through: 2.3.8 + tinyglobby: 0.2.12 + typescript: 5.8.2 + upath: 2.0.1 + uuid: 11.1.1 + validate-npm-package-license: 3.0.4 + validate-npm-package-name: 6.0.2 + wide-align: 1.1.5 + write-file-atomic: 5.0.1 + write-pkg: 4.0.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - '@types/node' + - babel-plugin-macros + - supports-color + + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + + libnpmaccess@10.0.3(supports-color@7.2.0): + dependencies: + npm-package-arg: 13.0.1 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + libnpmpublish@11.1.2(supports-color@7.2.0): + dependencies: + '@npmcli/package-json': 7.0.2 + ci-info: 4.4.0 + npm-package-arg: 13.0.1 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + proc-log: 5.0.0 + semver: 7.7.4 + sigstore: 4.1.1(supports-color@7.2.0) + ssri: 12.0.0 + transitivePeerDependencies: + - supports-color + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lines-and-columns@2.0.3: {} + + linkify-it@5.0.2: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + load-json-file@6.2.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 5.2.0 + strip-bom: 4.0.0 + type-fest: 0.6.0 + + load-json-file@7.0.1: {} + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.defaults@4.2.0: {} + + lodash.get@4.4.2: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.ismatch@4.4.0: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + lru-cache@11.2.6: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-fetch-happen@15.0.2(supports-color@7.2.0): + dependencies: + '@npmcli/agent': 4.0.0(supports-color@7.2.0) + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 4.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + ssri: 12.0.0 + transitivePeerDependencies: + - supports-color + + make-fetch-happen@15.0.3(supports-color@7.2.0): + dependencies: + '@npmcli/agent': 4.0.0(supports-color@7.2.0) + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 5.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + promise-retry: 2.0.1 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + + make-fetch-happen@15.0.6(supports-color@7.2.0): + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.0(supports-color@7.2.0) + '@npmcli/redact': 4.0.0 + cacache: 20.0.3 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 5.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + + map-obj@1.0.1: {} + + map-obj@4.3.0: {} + + markdown-it@14.2.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + matcher@6.0.0: + dependencies: + escape-string-regexp: 5.0.0 + + math-intrinsics@1.1.0: {} + + md5-hex@3.0.1: + dependencies: + blueimp-md5: 2.19.0 + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memoize@10.2.0: + dependencies: + mimic-function: 5.0.1 + + meow@14.0.0: {} + + meow@8.1.2: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.9 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.4: + dependencies: + brace-expansion: 1.1.18 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + + minimist@1.2.8: {} + + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@4.0.1: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 3.1.0 + optionalDependencies: + encoding: 0.1.13 + + minipass-fetch@5.0.1: + dependencies: + minipass: 7.1.2 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.2 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mkdirp-classic@0.5.3: {} + + mockdate@3.0.5: {} + + modify-values@1.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multimatch@5.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.4 + + mute-stream@2.0.0: {} + + nanoid@3.3.17: {} + + nanoid@3.3.18: {} + + napi-build-utils@2.0.0: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + next@16.3.3(@types/node@22.19.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@next/env': 16.3.3 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + postcss: 8.5.23 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.3 + '@next/swc-darwin-x64': 16.3.3 + '@next/swc-linux-arm64-gnu': 16.3.3 + '@next/swc-linux-arm64-musl': 16.3.3 + '@next/swc-linux-x64-gnu': 16.3.3 + '@next/swc-linux-x64-musl': 16.3.3 + '@next/swc-win32-arm64-msvc': 16.3.3 + '@next/swc-win32-x64-msvc': 16.3.3 + sharp: 0.35.4(@types/node@22.19.11) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + nise@5.1.9: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 11.3.1 + '@sinonjs/text-encoding': 0.7.3 + just-extend: 6.2.0 + path-to-regexp: 6.3.0 + + node-abi@3.87.0: + dependencies: + semver: 7.8.5 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp-build@4.8.4: {} + + node-gyp@12.2.0(supports-color@7.2.0): + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 15.0.3(supports-color@7.2.0) + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.5 + tar: 7.5.22 + tinyglobby: 0.2.12 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.4 + pstree.remy: 1.1.8 + semver: 7.7.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nofilter@3.1.0: {} + + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.11 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.8.5 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + npm-bundled@4.0.0: + dependencies: + npm-normalize-package-bin: 4.0.0 + + npm-bundled@5.0.0: + dependencies: + npm-normalize-package-bin: 5.0.0 + + npm-install-checks@7.1.2: + dependencies: + semver: 7.8.5 + + npm-install-checks@8.0.0: + dependencies: + semver: 7.8.5 + + npm-normalize-package-bin@4.0.0: {} + + npm-normalize-package-bin@5.0.0: {} + + npm-package-arg@12.0.2: + dependencies: + hosted-git-info: 8.1.0 + proc-log: 5.0.0 + semver: 7.8.5 + validate-npm-package-name: 6.0.2 + + npm-package-arg@13.0.1: + dependencies: + hosted-git-info: 9.0.2 + proc-log: 5.0.0 + semver: 7.7.4 + validate-npm-package-name: 6.0.2 + + npm-packlist@10.0.3: + dependencies: + ignore-walk: 8.0.0 + proc-log: 6.1.0 + + npm-pick-manifest@10.0.0: + dependencies: + npm-install-checks: 7.1.2 + npm-normalize-package-bin: 4.0.0 + npm-package-arg: 12.0.2 + semver: 7.8.5 + + npm-pick-manifest@11.0.3: + dependencies: + npm-install-checks: 8.0.0 + npm-normalize-package-bin: 5.0.0 + npm-package-arg: 13.0.1 + semver: 7.8.5 + + npm-registry-fetch@19.1.0(supports-color@7.2.0): + dependencies: + '@npmcli/redact': 3.2.2 + jsonparse: 1.3.1 + make-fetch-happen: 15.0.3(supports-color@7.2.0) + minipass: 7.1.2 + minipass-fetch: 4.0.1 + minizlib: 3.1.0 + npm-package-arg: 13.0.1 + proc-log: 5.0.0 + transitivePeerDependencies: + - supports-color + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nx@22.7.8: + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@emnapi/wasi-threads': 1.0.4 + '@jest/diff-sequences': 30.0.1 + '@napi-rs/wasm-runtime': 0.2.4 + '@tybys/wasm-util': 0.9.0 + '@yarnpkg/lockfile': 1.1.0 + '@zkochan/js-yaml': 0.0.7 + agent-base: 6.0.2(supports-color@7.2.0) + ansi-colors: 4.1.3 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + argparse: 2.0.1 + asynckit: 0.4.0 + axios: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + balanced-match: 4.0.3 + base64-js: 1.5.1 + bl: 4.1.0 + brace-expansion: 5.0.9 + buffer: 5.7.1 + call-bind-apply-helpers: 1.0.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + clone: 1.0.4 + color-convert: 2.0.1 + color-name: 1.1.4 + combined-stream: 1.0.8 + debug: 4.4.3(supports-color@7.2.0) + defaults: 1.0.4 + define-lazy-prop: 2.0.0 + delayed-stream: 1.0.0 + dotenv: 16.4.7 + dotenv-expand: 12.0.3 + dunder-proto: 1.0.1 + ejs: 5.0.1 + emoji-regex: 8.0.0 + end-of-stream: 1.4.5 + enquirer: 2.3.6 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + escalade: 3.2.0 + escape-string-regexp: 1.0.5 + figures: 3.2.0 + flat: 5.0.2 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + form-data: 4.0.6 + fs-constants: 1.0.0 + function-bind: 1.1.2 + get-caller-file: 2.0.5 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + has-flag: 4.0.0 + has-symbols: 1.1.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + https-proxy-agent: 5.0.1(supports-color@7.2.0) + ieee754: 1.2.1 + ignore: 7.0.5 + inherits: 2.0.4 + is-docker: 2.2.1 + is-fullwidth-code-point: 3.0.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + is-wsl: 2.2.0 + json5: 2.2.3 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + log-symbols: 4.1.0 + math-intrinsics: 1.1.0 + mime-db: 1.52.0 + mime-types: 2.1.35 + mimic-fn: 2.1.0 + minimatch: 10.2.5 + minimist: 1.2.8 + ms: 2.1.3 + npm-run-path: 4.0.1 + once: 1.4.0 + onetime: 5.1.2 + open: 8.4.2 + ora: 5.3.0 + path-key: 3.1.1 + picocolors: 1.1.1 + proxy-from-env: 2.1.0 + readable-stream: 3.6.2 + require-directory: 2.1.1 + resolve.exports: 2.0.3 + restore-cursor: 3.1.0 + safe-buffer: 5.2.1 + semver: 7.7.4 + signal-exit: 3.0.7 + smol-toml: 1.6.1 + string-width: 4.2.3 + string_decoder: 1.3.0 + strip-ansi: 6.0.1 + strip-bom: 3.0.0 + supports-color: 7.2.0 + tar-stream: 2.2.0 + tmp: 0.2.7 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + util-deprecate: 1.0.2 + wcwidth: 1.0.1 + wrap-ansi: 7.0.0 + wrappy: 1.0.2 + y18n: 5.0.8 + yaml: 2.9.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 22.7.8 + '@nx/nx-darwin-x64': 22.7.8 + '@nx/nx-freebsd-x64': 22.7.8 + '@nx/nx-linux-arm-gnueabihf': 22.7.8 + '@nx/nx-linux-arm64-gnu': 22.7.8 + '@nx/nx-linux-arm64-musl': 22.7.8 + '@nx/nx-linux-x64-gnu': 22.7.8 + '@nx/nx-linux-x64-musl': 22.7.8 + '@nx/nx-win32-arm64-msvc': 22.7.8 + '@nx/nx-win32-x64-msvc': 22.7.8 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + ora@5.3.0: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + orderedmap@2.1.1: {} + + p-finally@1.0.0: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map-series@2.1.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-map@7.0.4: {} + + p-pipe@3.1.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-reduce@2.1.0: {} + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + p-waterfall@2.1.1: + dependencies: + p-reduce: 2.1.0 + + package-config@5.0.0: + dependencies: + find-up-simple: 1.0.1 + load-json-file: 7.0.1 + + package-json-from-dist@1.0.1: {} + + pacote@21.0.1(supports-color@7.2.0): + dependencies: + '@npmcli/git': 6.0.3 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/promise-spawn': 8.0.3 + '@npmcli/run-script': 10.0.3(supports-color@7.2.0) + cacache: 20.0.3 + fs-minipass: 3.0.3 + minipass: 7.1.2 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-pick-manifest: 10.0.0 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + proc-log: 5.0.0 + promise-retry: 2.0.1 + sigstore: 4.1.1(supports-color@7.2.0) + ssri: 12.0.0 + tar: 7.5.22 + transitivePeerDependencies: + - supports-color + + pacote@21.3.1(supports-color@7.2.0): + dependencies: + '@npmcli/git': 7.0.1 + '@npmcli/installed-package-contents': 4.0.0 + '@npmcli/package-json': 7.0.2 + '@npmcli/promise-spawn': 9.0.1 + '@npmcli/run-script': 10.0.3(supports-color@7.2.0) + cacache: 20.0.3 + fs-minipass: 3.0.3 + minipass: 7.1.2 + npm-package-arg: 13.0.1 + npm-packlist: 10.0.3 + npm-pick-manifest: 11.0.3 + npm-registry-fetch: 19.1.0(supports-color@7.2.0) + proc-log: 6.1.0 + promise-retry: 2.0.1 + sigstore: 4.1.1(supports-color@7.2.0) + ssri: 13.0.1 + tar: 7.5.22 + transitivePeerDependencies: + - supports-color + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-conflict-json@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + just-diff: 6.0.2 + just-diff-apply: 5.5.0 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parse-path@7.1.0: + dependencies: + protocols: 2.0.2 + + parse-url@8.1.0: + dependencies: + parse-path: 7.1.0 + + parseurl@1.3.3: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-expression-matcher@1.6.2: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.2 + + path-to-regexp@0.1.13: {} + + path-to-regexp@6.3.0: {} + + path-type@3.0.0: + dependencies: + pify: 3.0.0 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pify@3.0.0: {} + + pify@4.0.1: {} + + pify@5.0.0: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + plur@6.0.0: + dependencies: + irregular-plurals: 4.2.0 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.23: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + pretty-format@30.2.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + proc-log@5.0.0: {} + + proc-log@6.1.0: {} + + process-nextick-args@2.0.1: {} + + proggy@3.0.0: {} + + promise-all-reject-late@1.0.1: {} + + promise-call-limit@3.0.2: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + promzard@2.0.0: + dependencies: + read: 4.1.0 + + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.11.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-gapcursor@1.4.0: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.2.0 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.6 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + + prosemirror-transform@1.11.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.6: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + protocols@2.0.2: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + pstree.remy@1.1.8: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode.js@2.3.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + queue-microtask@1.2.3: {} + + quick-lru@4.0.1: {} + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@18.3.1: {} + + react@19.2.4: {} + + read-cmd-shim@4.0.0: {} + + read-cmd-shim@5.0.0: {} + + read-pkg-up@3.0.0: + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + read@4.1.0: + dependencies: + mute-stream: 2.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + redis@4.7.1: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rimraf@6.1.2: + dependencies: + glob: 13.0.3 + package-json-from-dist: 1.0.1 + + rolldown-plugin-dts@0.22.3(rolldown@1.0.0-rc.6)(typescript@5.8.2): + dependencies: + '@babel/generator': 8.0.0-rc.2 + '@babel/helper-validator-identifier': 8.0.0-rc.2 + '@babel/parser': 8.0.0-rc.2 + '@babel/types': 8.0.0-rc.2 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.6 + obug: 2.1.1 + rolldown: 1.0.0-rc.6 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-rc.6: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.6 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.6 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.6 + '@rolldown/binding-darwin-x64': 1.0.0-rc.6 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.6 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.6 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.6 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.6 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.6 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.6 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.6 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.6 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.6 + + rope-sequence@1.3.4: {} + + run-async@4.0.6: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@5.7.2: {} + + semver@7.7.2: {} + + semver@7.7.4: {} + + semver@7.8.5: {} + + send@0.19.2(supports-color@5.5.0): + dependencies: + debug: 2.6.9(supports-color@5.5.0) + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + + serve-static@1.16.3(supports-color@5.5.0): + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + sharp@0.35.4(@types/node@22.19.11): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 22.19.11 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sigstore@4.1.1(supports-color@7.2.0): + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.0 + '@sigstore/sign': 4.1.1(supports-color@7.2.0) + '@sigstore/tuf': 4.0.2(supports-color@7.2.0) + '@sigstore/verify': 3.1.1 + transitivePeerDependencies: + - supports-color + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + sinon@12.0.1: + dependencies: + '@sinonjs/commons': 1.8.6 + '@sinonjs/fake-timers': 8.1.0 + '@sinonjs/samsam': 6.1.3 + diff: 5.2.2 + nise: 5.1.9 + supports-color: 7.2.0 + + slash@3.0.0: {} + + slash@5.1.0: {} + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + smart-buffer@4.2.0: {} + + smol-toml@1.6.1: {} + + socks-proxy-agent@8.0.5(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.4.0 + smart-buffer: 4.2.0 + + sort-keys@2.0.0: + dependencies: + is-plain-obj: 1.1.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + spawn-command@0.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + split2@3.2.2: + dependencies: + readable-stream: 3.6.2 + + split@1.0.1: + dependencies: + through: 2.3.8 + + sprintf-js@1.0.3: {} + + ssri@12.0.0: + dependencies: + minipass: 7.1.2 + + ssri@13.0.1: + dependencies: + minipass: 7.1.2 + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + standard-as-callback@2.1.0: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + + styled-jsx@5.1.6(react@19.2.4): + dependencies: + client-only: 0.0.1 + react: 19.2.4 + + supertap@3.0.1: + dependencies: + indent-string: 5.0.0 + js-yaml: 3.15.1 + serialize-error: 7.0.1 + strip-ansi: 7.1.2 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-dir@1.0.0: {} + + temp-dir@3.0.0: {} + + text-extensions@1.9.0: {} + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through@2.3.8: {} + + time-zone@1.0.0: {} + + tinyglobby@0.2.12: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tmp@0.2.7: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + treeverse@3.0.0: {} + + trim-newlines@3.0.1: {} + + ts.cryptojs256@1.0.1: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsscmp@1.0.6: {} + + tuf-js@4.1.0(supports-color@7.2.0): + dependencies: + '@tufjs/models': 4.1.0 + debug: 4.4.3(supports-color@7.2.0) + make-fetch-happen: 15.0.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-detect@4.0.8: {} + + type-detect@4.1.0: {} + + type-fest@0.13.1: {} + + type-fest@0.18.1: {} + + type-fest@0.4.1: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript@5.8.2: {} + + uc.micro@2.1.0: {} + + uglify-js@3.19.3: + optional: true + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unicorn-magic@0.4.0: {} + + unique-filename@5.0.0: + dependencies: + unique-slug: 6.0.0 + + unique-slug@6.0.0: + dependencies: + imurmurhash: 0.1.4 + + universal-user-agent@6.0.1: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + upath@2.0.1: {} + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.1.1: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@6.0.2: {} + + vary@1.1.2: {} + + w3c-keyname@2.2.8: {} + + walk-up-path@4.0.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + well-known-symbols@2.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + write-file-atomic@2.4.3: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + write-file-atomic@6.0.0: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + write-file-atomic@7.0.1: + dependencies: + signal-exit: 4.1.0 + + write-json-file@3.2.0: + dependencies: + detect-indent: 5.0.0 + graceful-fs: 4.2.11 + make-dir: 2.1.0 + pify: 4.0.1 + sort-keys: 2.0.0 + write-file-atomic: 2.4.3 + + write-pkg@4.0.0: + dependencies: + sort-keys: 2.0.0 + type-fest: 0.4.1 + write-json-file: 3.2.0 + + ws@7.5.13: {} + + ws@8.21.0: {} + + xml-naming@0.3.0: {} + + xtend@4.0.2: {} + + y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.6 + y-protocols: 1.0.7(yjs@13.6.29) + yjs: 13.6.29 + + y-protocols@1.0.7(yjs@13.6.29): + dependencies: + lib0: 0.2.117 + yjs: 13.6.29 + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yaml@2.9.0: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yjs@13.6.29: + dependencies: + lib0: 0.2.117 + + yoctocolors-cjs@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6c2bb3e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,34 @@ +packages: + - "playground/*" + - "packages/*" + - "tests" +allowBuilds: + better-sqlite3: true + nx: true + sharp: true +overrides: + '@hono/node-server@<2.0.5': ^2.0.5 + brace-expansion@>=4.0.0 <5.0.9: ^5.0.9 + fast-xml-parser@<5.7.0: ^5.7.0 + fast-xml-parser@>=5.0.0 <5.3.5: ^5.3.5 + fast-xml-parser@>=5.0.0 <5.3.6: ^5.3.6 + fast-xml-parser@>=5.0.0 <5.3.8: ^5.3.8 + fast-xml-parser@>=5.0.0 <5.5.6: ^5.5.6 + fast-xml-parser@>=5.0.0 <5.5.7: ^5.5.7 + js-yaml@>=4.0.0 <4.3.0: ^4.3.0 + js-yaml@>=4.0.0 <=4.1.1: ^4.1.2 + minimatch@>=10.0.0 <10.2.1: ^10.2.1 + minimatch@>=10.0.0 <10.2.3: ^10.2.3 + postcss@<8.5.10: ^8.5.10 + postcss@<=8.5.11: ^8.5.12 + postcss@<=8.5.17: ^8.5.18 + postcss@<=8.5.22: ^8.5.23 + qs@>=6.11.1 <=6.15.1: ^6.15.2 + sharp@<0.35.0: ^0.35.0 + tar@<=7.5.10: ^7.5.11 + tar@<=7.5.15: ^7.5.16 + tar@<=7.5.16: ^7.5.17 + tar@<=7.5.17: ^7.5.18 + tar@<=7.5.18: ^7.5.19 + tar@<=7.5.20: ^7.5.21 + tar@<=7.5.9: ^7.5.10 diff --git a/rolldown.config.js b/rolldown.config.js new file mode 100644 index 0000000..1f15cf6 --- /dev/null +++ b/rolldown.config.js @@ -0,0 +1,131 @@ +import { builtinModules } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "rolldown"; +import { dts } from "rolldown-plugin-dts"; + +const nodeBuiltins = new Set(builtinModules); + +function nodeProtocolPlugin() { + return { + name: "node-protocol", + resolveId(source) { + if (nodeBuiltins.has(source)) { + return { id: `node:${source}`, external: true }; + } + }, + }; +} + +function defineVersionPlugin(version) { + const replacement = JSON.stringify(version); + return { + name: "define-version", + transform(code, id) { + if (code.includes("__HOCUSPOCUS_VERSION__")) { + return { code: code.replaceAll("__HOCUSPOCUS_VERSION__", replacement) }; + } + }, + }; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function getWorkspacePackages() { + const packagePaths = fs.globSync("packages/*/package.json", { cwd: __dirname }); + return packagePaths + .map((pkgPath) => { + const fullPath = path.join(__dirname, pkgPath); + const pkgJson = JSON.parse(fs.readFileSync(fullPath, "utf-8")); + return { location: path.dirname(fullPath), package: pkgJson }; + }) + .filter((pkg) => pkg.package.exports && !["@hocuspocus/docs", "@hocuspocus/demo"].includes(pkg.package.name)); +} + +function getExternalDependencies(pkgJson) { + return [ + ...Object.keys(pkgJson.dependencies || {}), + ...Object.keys(pkgJson.peerDependencies || {}), + ...Object.keys(pkgJson.devDependencies || {}), + ]; +} + +// Match a package name or any of its sub-paths (e.g. `react` and `react/jsx-runtime`). +function isExternal(id, packages) { + return packages.some((pkg) => id === pkg || id.startsWith(`${pkg}/`)); +} + +// Per-package list of dependencies to bundle into the CJS output instead of leaving +// external. Use this for ESM-only dependencies whose `require()` would fail under +// CJS-only module loaders (e.g. Jest 29, ts-node default). They remain external in +// the ESM build so consumers can still tree-shake / dedupe via node_modules. +const cjsInlineDeps = { + "@hocuspocus/server": ["crossws"], +}; + +const packages = getWorkspacePackages(); + +// Build aliases dynamically from all packages +const aliases = Object.fromEntries( + packages.map((pkg) => [pkg.package.name, path.join(pkg.location, "src")]), +); + +const configs = packages.flatMap((pkg) => { + const basePath = pkg.location; + const input = path.join(basePath, "src/index.ts"); + const { name, exports } = pkg.package; + const external = getExternalDependencies(pkg.package); + const inlineInCjs = cjsInlineDeps[name] ?? []; + const cjsExternal = external.filter((dep) => !inlineInCjs.includes(dep)); + + // ESM bundle: all deps external. + const esmConfig = defineConfig({ + input, + external: (id) => id.startsWith("node:") || isExternal(id, external), + plugins: [nodeProtocolPlugin(), defineVersionPlugin(pkg.package.version)], + output: { + file: path.join(basePath, exports.default.import), + format: "esm", + sourcemap: true, + }, + resolve: { + alias: aliases, + }, + }); + + // CJS bundle: same as ESM, but inline any ESM-only deps listed in + // `cjsInlineDeps` so consumers on CJS-only module loaders (Jest 29, ts-node + // default) don't have to require() native ESM. + const cjsConfig = defineConfig({ + input, + external: (id) => id.startsWith("node:") || isExternal(id, cjsExternal), + plugins: [nodeProtocolPlugin(), defineVersionPlugin(pkg.package.version)], + output: { + file: path.join(basePath, exports.default.require), + format: "cjs", + sourcemap: true, + exports: "auto", + }, + resolve: { + alias: aliases, + }, + }); + + // DTS config for type declarations + // Don't use aliases here - we want to reference external package types, not inline them + const dtsConfig = defineConfig({ + input, + external: (id) => id.startsWith("node:") || isExternal(id, external), + plugins: [nodeProtocolPlugin(), dts({ emitDtsOnly: true })], + output: { + dir: path.join(basePath, "dist"), + format: "esm", + }, + }); + + return [esmConfig, cjsConfig, dtsConfig]; +}); + +export default configs; diff --git a/tests/CHANGELOG.md b/tests/CHANGELOG.md new file mode 100644 index 0000000..003e806 --- /dev/null +++ b/tests/CHANGELOG.md @@ -0,0 +1,891 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.4.6-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.6-rc.0...v3.4.6-rc.1) (2026-02-03) + +**Note:** Version bump only for package tests + + + + + +## [3.4.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.5-rc.0...v3.4.6-rc.0) (2026-02-03) + +**Note:** Version bump only for package tests + + + + + +## [3.4.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.4.4...v3.4.5-rc.0) (2026-02-03) + +**Note:** Version bump only for package tests + + + + + +## [3.4.4](https://github.com/ueberdosis/hocuspocus/compare/v3.4.3...v3.4.4) (2026-01-25) + +**Note:** Version bump only for package tests + + + + + +## [3.4.3](https://github.com/ueberdosis/hocuspocus/compare/v3.4.2...v3.4.3) (2025-12-15) + +**Note:** Version bump only for package tests + + + + + +## [3.4.2](https://github.com/ueberdosis/hocuspocus/compare/v3.4.1...v3.4.2) (2025-12-15) + +**Note:** Version bump only for package tests + + + + + +## [3.4.1](https://github.com/ueberdosis/hocuspocus/compare/v3.4.0...v3.4.1) (2025-12-09) + +**Note:** Version bump only for package tests + + + + + +# [3.4.0](https://github.com/ueberdosis/hocuspocus/compare/v3.3.2...v3.4.0) (2025-10-24) + +**Note:** Version bump only for package tests + + + + + +## [3.3.2](https://github.com/ueberdosis/hocuspocus/compare/v3.3.1...v3.3.2) (2025-10-23) + +**Note:** Version bump only for package tests + + + + + +## [3.3.1](https://github.com/ueberdosis/hocuspocus/compare/v3.3.0...v3.3.1) (2025-10-22) + +**Note:** Version bump only for package tests + + + + + +# [3.3.0](https://github.com/ueberdosis/hocuspocus/compare/v3.2.6...v3.3.0) (2025-10-22) + + +### Features + +* add onTokenSync hook for auth token re-sync ([#1001](https://github.com/ueberdosis/hocuspocus/issues/1001)) ([ee252fb](https://github.com/ueberdosis/hocuspocus/commit/ee252fb99ea6faf80cc71d2e1ce51726e698b76b)) + + + + + +## [3.2.6](https://github.com/ueberdosis/hocuspocus/compare/v3.2.5...v3.2.6) (2025-10-17) + +**Note:** Version bump only for package tests + + + + + +## [3.2.5](https://github.com/ueberdosis/hocuspocus/compare/v3.2.4...v3.2.5) (2025-10-07) + + +### Bug Fixes + +* delay adding document to instance until connection is about to be established ([#972](https://github.com/ueberdosis/hocuspocus/issues/972)) ([1e2917a](https://github.com/ueberdosis/hocuspocus/commit/1e2917aacb342c035645bb3c01af4910d699eaad)) + + + + + +## [3.2.4](https://github.com/ueberdosis/hocuspocus/compare/v3.2.3...v3.2.4) (2025-09-30) + +**Note:** Version bump only for package tests + + + + + +## [3.2.3](https://github.com/ueberdosis/hocuspocus/compare/v3.2.2...v3.2.3) (2025-08-06) + +**Note:** Version bump only for package tests + + + + + +## [3.2.2](https://github.com/ueberdosis/hocuspocus/compare/v3.2.1...v3.2.2) (2025-07-28) + +**Note:** Version bump only for package tests + + + + + +## [3.2.1](https://github.com/ueberdosis/hocuspocus/compare/v3.2.0...v3.2.1) (2025-07-20) + +**Note:** Version bump only for package tests + + + + + +# [3.2.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.12...v3.2.0) (2025-07-12) + +**Note:** Version bump only for package tests + + + + + +## [3.1.12](https://github.com/ueberdosis/hocuspocus/compare/v3.1.11...v3.1.12) (2025-07-09) + +**Note:** Version bump only for package tests + + + + + +## [3.1.10](https://github.com/ueberdosis/hocuspocus/compare/v3.1.9...v3.1.10) (2025-07-08) + +**Note:** Version bump only for package tests + + + + + +## [3.1.8](https://github.com/ueberdosis/hocuspocus/compare/v3.1.7...v3.1.8) (2025-07-01) + + +### Features + +* beforeUnloadDocument: allow passing in Hocuspocus constructor ; allow preventing unload by throwing an error in beforeUnloadDocument ([63b298e](https://github.com/ueberdosis/hocuspocus/commit/63b298eddf23b8f472a066dc8274b22d790176ea)) + + + + + +## [3.1.7](https://github.com/ueberdosis/hocuspocus/compare/v3.1.6...v3.1.7) (2025-06-22) + +**Note:** Version bump only for package tests + + + + + +## [3.1.6](https://github.com/ueberdosis/hocuspocus/compare/v3.1.5...v3.1.6) (2025-06-20) + +**Note:** Version bump only for package tests + + + + + +## [3.1.5](https://github.com/ueberdosis/hocuspocus/compare/v3.1.4...v3.1.5) (2025-06-19) + +**Note:** Version bump only for package tests + + + + + +## [3.1.4](https://github.com/ueberdosis/hocuspocus/compare/v3.1.3...v3.1.4) (2025-06-17) + +**Note:** Version bump only for package tests + + + + + +## [3.1.3](https://github.com/ueberdosis/hocuspocus/compare/v3.1.2...v3.1.3) (2025-06-06) + +**Note:** Version bump only for package tests + + + + + +## [3.1.2](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1...v3.1.2) (2025-06-05) + +**Note:** Version bump only for package tests + + + + + +## [3.1.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.1...v3.1.1) (2025-05-10) + +**Note:** Version bump only for package tests + + + + + +## [3.1.1-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v3.1.1-rc.0...v3.1.1-rc.1) (2025-05-08) + +**Note:** Version bump only for package tests + + + + + +## [3.1.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0...v3.1.1-rc.0) (2025-04-30) + +**Note:** Version bump only for package tests + + + + + +# [3.1.0](https://github.com/ueberdosis/hocuspocus/compare/v3.1.0-rc.0...v3.1.0) (2025-04-29) + +**Note:** Version bump only for package tests + + + + + +# [3.1.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.8-rc.0...v3.1.0-rc.0) (2025-04-28) + +**Note:** Version bump only for package tests + + + + + +## [3.0.8-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.7-rc.0...v3.0.8-rc.0) (2025-04-09) + +**Note:** Version bump only for package tests + + + + + +## [3.0.7-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.6-rc.0...v3.0.7-rc.0) (2025-04-09) + +**Note:** Version bump only for package tests + + + + + +## [3.0.6-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.5-rc.0...v3.0.6-rc.0) (2025-03-28) + +**Note:** Version bump only for package tests + + + + + +## [3.0.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.4-rc.0...v3.0.5-rc.0) (2025-03-28) + + +### Features + +* add beforeSync hook ([#919](https://github.com/ueberdosis/hocuspocus/issues/919)) ([a6a7bcd](https://github.com/ueberdosis/hocuspocus/commit/a6a7bcd0768378908ffb5d32096183280115631b)) + + + + + +## [3.0.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v3.0.3-rc.0...v3.0.4-rc.0) (2025-03-20) + +**Note:** Version bump only for package tests + + + + + +## [3.0.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.3-rc.0) (2025-03-20) + +**Note:** Version bump only for package tests + + + + + +## [3.0.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.2-rc.0) (2025-03-13) + +**Note:** Version bump only for package tests + + + + + +## [3.0.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.1-rc.0) (2025-03-13) + +**Note:** Version bump only for package tests + + + + + +# [3.0.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.2...v3.0.0) (2025-03-13) + +**Note:** Version bump only for package tests + + + + + +## [2.15.2](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1...v2.15.2) (2025-02-03) + +**Note:** Version bump only for package tests + + + + + +## [2.15.1](https://github.com/ueberdosis/hocuspocus/compare/v2.15.1-rc.0...v2.15.1) (2025-01-29) + +**Note:** Version bump only for package tests + + + + + +## [2.15.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.15.0...v2.15.1-rc.0) (2025-01-29) + +**Note:** Version bump only for package tests + + + + + +# [2.15.0](https://github.com/ueberdosis/hocuspocus/compare/v2.14.0...v2.15.0) (2024-12-10) + +**Note:** Version bump only for package tests + + + + + +# [2.14.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.7...v2.14.0) (2024-11-20) + +**Note:** Version bump only for package tests + + + + + +## [2.13.7](https://github.com/ueberdosis/hocuspocus/compare/v2.13.6...v2.13.7) (2024-10-08) + +**Note:** Version bump only for package tests + + + + + +## [2.13.6](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5...v2.13.6) (2024-09-19) + +**Note:** Version bump only for package tests + + + + + +## [2.13.5](https://github.com/ueberdosis/hocuspocus/compare/v2.13.5-rc.0...v2.13.5) (2024-07-02) + +**Note:** Version bump only for package tests + + + + + +## [2.13.5-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.4-rc.0...v2.13.5-rc.0) (2024-07-01) + +**Note:** Version bump only for package tests + + + + + +## [2.13.4-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.4-rc.0) (2024-07-01) + +**Note:** Version bump only for package tests + + + + + +## [2.13.3-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.13.2...v2.13.3-rc.0) (2024-07-01) + +**Note:** Version bump only for package tests + + + + + +## [2.13.2](https://github.com/ueberdosis/hocuspocus/compare/v2.13.1...v2.13.2) (2024-06-14) + +**Note:** Version bump only for package tests + + + + + +## [2.13.1](https://github.com/ueberdosis/hocuspocus/compare/v2.13.0...v2.13.1) (2024-06-06) + +**Note:** Version bump only for package tests + + + + + +# [2.13.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.3...v2.13.0) (2024-05-18) + +**Note:** Version bump only for package tests + + + + + +## [2.12.3](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2...v2.12.3) (2024-05-16) + +**Note:** Version bump only for package tests + + + + + +## [2.12.2](https://github.com/ueberdosis/hocuspocus/compare/v2.12.2-rc.0...v2.12.2) (2024-04-25) + +**Note:** Version bump only for package tests + + + + + +## [2.12.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.1-rc.0...v2.12.2-rc.0) (2024-03-29) + +**Note:** Version bump only for package tests + + + + + +## [2.12.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.12.0-rc.0...v2.12.1-rc.0) (2024-03-29) + +**Note:** Version bump only for package tests + + + + + +# [2.12.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.11.3...v2.12.0-rc.0) (2024-03-24) + +**Note:** Version bump only for package tests + + + + + +## [2.11.3](https://github.com/ueberdosis/hocuspocus/compare/v2.11.2...v2.11.3) (2024-03-02) + +**Note:** Version bump only for package tests + + + + + +## [2.11.2](https://github.com/ueberdosis/hocuspocus/compare/v2.11.1...v2.11.2) (2024-02-16) + +**Note:** Version bump only for package tests + + + + + +## [2.11.1](https://github.com/ueberdosis/hocuspocus/compare/v2.11.0...v2.11.1) (2024-02-11) + +**Note:** Version bump only for package tests + + + + + +# [2.11.0](https://github.com/ueberdosis/hocuspocus/compare/v2.10.0...v2.11.0) (2024-02-05) + +**Note:** Version bump only for package tests + + + + + +# [2.10.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.2-rc.0...v2.10.0) (2024-01-31) + +**Note:** Version bump only for package tests + + + + + +## [2.9.2-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.1-rc.0...v2.9.2-rc.0) (2024-01-22) + +**Note:** Version bump only for package tests + + + + + +## [2.9.1-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0...v2.9.1-rc.0) (2024-01-18) + +**Note:** Version bump only for package tests + + + + + +# [2.9.0](https://github.com/ueberdosis/hocuspocus/compare/v2.9.0-rc.0...v2.9.0) (2024-01-05) + +**Note:** Version bump only for package tests + + + + + +# [2.9.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.8.1...v2.9.0-rc.0) (2024-01-04) + +**Note:** Version bump only for package tests + + + + + +## [2.8.1](https://github.com/ueberdosis/hocuspocus/compare/v2.8.0...v2.8.1) (2023-11-21) + +**Note:** Version bump only for package tests + + + + + +# [2.8.0](https://github.com/ueberdosis/hocuspocus/compare/v2.7.1...v2.8.0) (2023-11-15) + +**Note:** Version bump only for package tests + + + + + +## [2.7.1](https://github.com/ueberdosis/hocuspocus/compare/v2.7.0...v2.7.1) (2023-10-19) + +**Note:** Version bump only for package tests + + + + + +# [2.7.0](https://github.com/ueberdosis/hocuspocus/compare/v2.6.1...v2.7.0) (2023-10-16) + +**Note:** Version bump only for package tests + + + + + +## [2.6.1](https://github.com/ueberdosis/hocuspocus/compare/v2.6.0...v2.6.1) (2023-10-05) + +**Note:** Version bump only for package tests + + + + + +# [2.6.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0...v2.6.0) (2023-10-04) + +**Note:** Version bump only for package tests + + + + + +# [2.5.0](https://github.com/ueberdosis/hocuspocus/compare/v2.5.0-rc.0...v2.5.0) (2023-09-06) + +**Note:** Version bump only for package tests + + + + + +# [2.5.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0...v2.5.0-rc.0) (2023-08-24) + +**Note:** Version bump only for package tests + + + + + +# [2.4.0](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.1...v2.4.0) (2023-08-17) + +**Note:** Version bump only for package tests + + + + + +# [2.4.0-rc.1](https://github.com/ueberdosis/hocuspocus/compare/v2.4.0-rc.0...v2.4.0-rc.1) (2023-08-12) + +**Note:** Version bump only for package tests + + + + + +# [2.4.0-rc.0](https://github.com/ueberdosis/hocuspocus/compare/v2.3.1...v2.4.0-rc.0) (2023-08-12) + +**Note:** Version bump only for package tests + + + + + +## [2.3.1](https://github.com/ueberdosis/hocuspocus/compare/v2.3.0...v2.3.1) (2023-08-01) + +**Note:** Version bump only for package tests + + + + + +# [2.3.0](https://github.com/ueberdosis/hocuspocus/compare/v2.2.3...v2.3.0) (2023-07-31) + +**Note:** Version bump only for package tests + + + + + +## [2.2.3](https://github.com/ueberdosis/hocuspocus/compare/v2.2.2...v2.2.3) (2023-07-14) + +**Note:** Version bump only for package tests + + + + + +## [2.2.2](https://github.com/ueberdosis/hocuspocus/compare/v2.2.1...v2.2.2) (2023-07-14) + +**Note:** Version bump only for package tests + + + + + +## [2.2.1](https://github.com/ueberdosis/hocuspocus/compare/v2.2.0...v2.2.1) (2023-07-04) + +**Note:** Version bump only for package tests + + + + + +# [2.2.0](https://github.com/ueberdosis/hocuspocus/compare/v2.1.0...v2.2.0) (2023-06-22) + +**Note:** Version bump only for package tests + + + + + +# [2.1.0](https://github.com/ueberdosis/hocuspocus/compare/v2.0.6...v2.1.0) (2023-06-03) + +**Note:** Version bump only for package tests + + + + + +## [2.0.6](https://github.com/ueberdosis/hocuspocus/compare/v2.0.5...v2.0.6) (2023-04-25) + +**Note:** Version bump only for package tests + + + + + +## [2.0.5](https://github.com/ueberdosis/hocuspocus/compare/v2.0.4...v2.0.5) (2023-04-24) + +**Note:** Version bump only for package tests + + + + + +## [2.0.4](https://github.com/ueberdosis/hocuspocus/compare/v2.0.3...v2.0.4) (2023-04-23) + +**Note:** Version bump only for package tests + + + + + +## [2.0.3](https://github.com/ueberdosis/hocuspocus/compare/v2.0.2...v2.0.3) (2023-04-05) + +**Note:** Version bump only for package tests + + + + + +## [2.0.2](https://github.com/ueberdosis/hocuspocus/compare/v2.0.1...v2.0.2) (2023-04-04) + +**Note:** Version bump only for package tests + + + + + +## [2.0.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0...v2.0.1) (2023-03-30) + +**Note:** Version bump only for package tests + + + + + +# [2.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.3...v2.0.0) (2023-03-29) + +**Note:** Version bump only for package tests + + + + + +# [2.0.0-beta.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.1...v2.0.0-beta.0) (2023-03-28) + +**Note:** Version bump only for package tests + + + + + +# [2.0.0-alpha.1](https://github.com/ueberdosis/hocuspocus/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2023-03-23) + +**Note:** Version bump only for package tests + + + + + +# [2.0.0-alpha.0](https://github.com/ueberdosis/hocuspocus/compare/v1.1.0...v2.0.0-alpha.0) (2023-02-28) + +**Note:** Version bump only for package tests + + + + + +# [1.1.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.2...v1.1.0) (2023-02-24) + +**Note:** Version bump only for package tests + + + + + +## [1.0.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.1...v1.0.2) (2023-02-16) + +**Note:** Version bump only for package tests + + + + + +## [1.0.1](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0...v1.0.1) (2023-01-30) + +**Note:** Version bump only for package tests + + + + + +# [1.0.0](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.7...v1.0.0) (2023-01-18) + +**Note:** Version bump only for package tests + + + + + +# [1.0.0-beta.7](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2023-01-11) + +**Note:** Version bump only for package tests + + + + + +# [1.0.0-beta.6](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-12-03) + +**Note:** Version bump only for package tests + + + + + +# [1.0.0-beta.5](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.5) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.4](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-11-25) + + +### Bug Fixes + +* Crash when websocket receives out of bound characters pre-auth ([#444](https://github.com/ueberdosis/hocuspocus/issues/444)) ([73d8a48](https://github.com/ueberdosis/hocuspocus/commit/73d8a4812a4d9a851c97a52b6d62180a1fa4b56b)) + + + + + +# [1.0.0-beta.3](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-11-03) + +**Note:** Version bump only for package tests + + + + + +# [1.0.0-beta.2](https://github.com/ueberdosis/hocuspocus/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-09-28) + +**Note:** Version bump only for package tests + + + + + +# 1.0.0-beta.111 (2022-09-28) + + +### Bug Fixes + +* Potential onCreateDocument race condition ([#167](https://github.com/ueberdosis/hocuspocus/issues/167)) ([b3e3e4d](https://github.com/ueberdosis/hocuspocus/commit/b3e3e4dea74f9b833ccb0c6a6521f55c001411c1)) + + +### Features + +* Add connectionsCount and documentsCount methods to server ([8bdbcd8](https://github.com/ueberdosis/hocuspocus/commit/8bdbcd86b1f18462f6636b75a4cbd97ebefdb227)) +* Message Authentication ([#163](https://github.com/ueberdosis/hocuspocus/issues/163)) ([a1e68d5](https://github.com/ueberdosis/hocuspocus/commit/a1e68d5a272742bd17dd92522dfc908277343849)) diff --git a/tests/extension-database/fetch.ts b/tests/extension-database/fetch.ts new file mode 100644 index 0000000..a14027e --- /dev/null +++ b/tests/extension-database/fetch.ts @@ -0,0 +1,56 @@ +import test from 'ava' +// eslint-disable-next-line import/no-extraneous-dependencies +import { Database } from '@hocuspocus/extension-database' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('fetch has the document name', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + extensions: [ + new Database({ + async fetch({ documentName }) { + t.is(documentName, 'my-unique-document-name') + + resolve('done') + + return null + }, + }), + ], + }) + + newHocuspocusProvider(t, server, { + name: 'my-unique-document-name', + }) + }) +}) + +test('passes context from onAuthenticate to fetch', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + extensions: [ + new Database({ + async fetch({ context }) { + t.deepEqual(context, { + user: 123, + }) + + resolve('done') + + return null + }, + }), + ], + async onAuthenticate() { + return { + user: 123, + } + }, + }) + + newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + name: 'my-unique-document-name', + }) + }) +}) diff --git a/tests/extension-logger/onListen.ts b/tests/extension-logger/onListen.ts new file mode 100644 index 0000000..8cf52b6 --- /dev/null +++ b/tests/extension-logger/onListen.ts @@ -0,0 +1,83 @@ +import test from 'ava' +import sinon from 'sinon' +import { Logger } from '@hocuspocus/extension-logger' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +const fakeLogger = (message: any) => { +} + +test('logs something', async t => { + await new Promise(async resolve => { + const spy = sinon.spy(fakeLogger) + + const server = await newHocuspocus(t, { + extensions: [ + new Logger({ + log: spy, + }), + ], + }) + + newHocuspocusProvider(t, server, { + onConnect() { + t.true(spy.callCount > 1, 'Expected the Logger to log something, but didn\'t receive anything.') + t.true(spy.callCount === 3, `Expected it to log 11 times, but actually logged ${spy.callCount} times`) + + resolve('done') + }, + }) + }) +}) + +test('uses the global instance name', async t => { + await new Promise(async resolve => { + const spy = sinon.spy(fakeLogger) + + const hocuspocus = await newHocuspocus(t, { + name: 'FOOBAR123', + extensions: [ + new Logger({ + log: spy, + }), + ], + }) + + await hocuspocus.server!.destroy() + + t.is(spy.args[spy.args.length - 1][0].includes('FOOBAR123'), true, 'Expected the Logger to use the configured instance name.') + + resolve('done') + }) + +}) + +test('doesn\'t log anything if all messages are disabled', async t => { + await new Promise(async resolve => { + const spy = sinon.spy(fakeLogger) + + const hocuspocus = await newHocuspocus(t, { + extensions: [ + new Logger({ + log: spy, + // TODO: Those hooks aren't triggered anyway. + // onLoadDocument: false, + // onChange: false, + // onConnect: false, + // onDisconnect: false, + // onUpgrade: false, + // onRequest: false, + // @ts-expect-error + onListen: false, + onDestroy: false, + onConfigure: false, + }), + ], + }) + + await hocuspocus.server!.destroy() + + t.is(spy.callCount, 0, 'Expected the Logger to not log anything.') + + resolve('done') + }) +}) diff --git a/tests/extension-redis/onAwarenessChange.ts b/tests/extension-redis/onAwarenessChange.ts new file mode 100644 index 0000000..5284ccd --- /dev/null +++ b/tests/extension-redis/onAwarenessChange.ts @@ -0,0 +1,106 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import type { onAwarenessChangeParameters } from "@hocuspocus/provider"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + redisConnectionSettings, +} from "../utils/index.ts"; + +test("syncs existing awareness state", async (t) => { + const documentName = `test-${crypto.randomUUID()}`; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }), + ], + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + }), + ], + }); + + const provider = newHocuspocusProvider(t, server, { + name: documentName, + onSynced() { + // Once we're set up, change the local Awareness state. + // The updated state then needs to go through Redis: + // provider -> server -> Redis -> anotherServer -> anotherProvider + provider.setAwarenessField("name", "first"); + + // Time to initialize a second provider, and connect to `anotherServer` + // to check whether existing Awareness states are synced through Redis. + newHocuspocusProvider(t, anotherServer, { + name: documentName, + onAwarenessChange({ states }: onAwarenessChangeParameters) { + // Wait until we have exactly 2 states with the expected data + const state = states.find( + (state) => state.clientId === provider.document.clientID, + ); + if (states.length === 2 && state?.name === "first") { + t.pass(); + resolve("done"); + } + }, + }); + }, + }); + }); +}); + +test("syncs awareness between servers and clients", async (t) => { + const documentName = `test-${crypto.randomUUID()}`; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }), + ], + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + }), + ], + }); + + const provider = newHocuspocusProvider(t, anotherServer, { + name: documentName, + onSynced() { + // once we're setup change awareness on provider, to get to client it will + // need to pass through the pubsub extension: + // provider -> anotherServer -> pubsub -> server -> client + provider.setAwarenessField("name", "second"); + }, + }); + + newHocuspocusProvider(t, server, { + name: documentName, + onAwarenessChange: ({ states }: onAwarenessChangeParameters) => { + // Wait until we have exactly 2 states with the expected data + const state = states.find( + (state) => state.clientId === provider.document.clientID, + ); + if (states.length === 2 && state?.name === "second") { + t.pass(); + resolve("done"); + } + }, + }); + }); +}); diff --git a/tests/extension-redis/onChange.ts b/tests/extension-redis/onChange.ts new file mode 100644 index 0000000..c2f0586 --- /dev/null +++ b/tests/extension-redis/onChange.ts @@ -0,0 +1,60 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + redisConnectionSettings, +} from "../utils/index.ts"; + +test("syncs updates between servers and clients", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }), + ], + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + }), + ], + }); + + // Once we’re setup make an edit on anotherProvider. To get to the provider it will need + // to pass through Redis: + // provider -> server -> Redis -> anotherServer -> anotherProvider + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray("foo").insert(0, ["bar"]); + }, + }); + + // Once the initial data is synced, wait for an additional update to check + // if both documents have the same content. + const anotherProvider = newHocuspocusProvider(t, anotherServer, { + onSynced() { + provider.on("message", () => { + if (resolved) return; + resolved = true; + + setTimeout(() => { + t.is( + provider.document.getArray("foo").get(0), + anotherProvider.document.getArray("foo").get(0), + ); + + resolve("done"); + }, 200); + }); + }, + }); + }); +}); diff --git a/tests/extension-redis/onStateless.ts b/tests/extension-redis/onStateless.ts new file mode 100644 index 0000000..d654d32 --- /dev/null +++ b/tests/extension-redis/onStateless.ts @@ -0,0 +1,232 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + redisConnectionSettings, +} from "../utils/index.ts"; + +test("syncs broadcast stateless message between servers and clients", async (t) => { + const redisPrefix = crypto.randomUUID(); + const payloadToSend = "STATELESS-MESSAGE"; + let resolvePayload!: (payload: string) => void; + const payloadReceived = new Promise((resolve) => { + resolvePayload = resolve; + }); + let resolveRemoteSynced!: () => void; + const remoteSynced = new Promise((resolve) => { + resolveRemoteSynced = resolve; + }); + let resolveSenderSynced!: () => void; + const senderSynced = new Promise((resolve) => { + resolveSenderSynced = resolve; + }); + + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + }); + + newHocuspocusProvider(t, anotherServer, { + onStateless({ payload }) { + resolvePayload(payload); + }, + onSynced() { + resolveRemoteSynced(); + }, + }); + newHocuspocusProvider(t, server, { + onSynced() { + resolveSenderSynced(); + }, + }); + + await Promise.all([remoteSynced, senderSynced]); + const document = server.documents.get("hocuspocus-test"); + if (!document) { + throw new Error("Expected the document to be loaded"); + } + document.broadcastStateless(payloadToSend); + + t.is(await payloadReceived, payloadToSend); +}); + +test("client stateless messages shouldnt propagate to other server", async (t) => { + const redisPrefix = crypto.randomUUID(); + const payloadToSend = "STATELESS-MESSAGE"; + const barrier = "redis-barrier"; + const remotePayloads: string[] = []; + let resolveLocalPayload!: (payload: string) => void; + const localPayloadReceived = new Promise((resolve) => { + resolveLocalPayload = resolve; + }); + let resolveRemoteSynced!: () => void; + const remoteSynced = new Promise((resolve) => { + resolveRemoteSynced = resolve; + }); + let resolveRemoteBarrier!: () => void; + const remoteBarrierReceived = new Promise((resolve) => { + resolveRemoteBarrier = resolve; + }); + let resolveSenderSynced!: () => void; + const senderSynced = new Promise((resolve) => { + resolveSenderSynced = resolve; + }); + + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + onStateless({ payload }) { + resolveLocalPayload(payload); + }, + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + }); + + newHocuspocusProvider(t, anotherServer, { + onSynced() { + resolveRemoteSynced(); + }, + onStateless({ payload }) { + remotePayloads.push(payload); + if (payload === barrier) { + resolveRemoteBarrier(); + } + }, + }); + const provider = newHocuspocusProvider(t, server, { + onSynced() { + resolveSenderSynced(); + }, + }); + + await Promise.all([remoteSynced, senderSynced]); + provider.sendStateless(payloadToSend); + t.is(await localPayloadReceived, payloadToSend); + + const document = server.documents.get("hocuspocus-test"); + if (!document) { + throw new Error("Expected the document to be loaded"); + } + document.broadcastStateless(barrier); + await remoteBarrierReceived; + + t.deepEqual(remotePayloads, [barrier]); +}); + +test("server client stateless messages shouldnt propagate to other client", async (t) => { + const redisPrefix = crypto.randomUUID(); + const response = "test123"; + const barrier = "redis-barrier"; + const remotePayloads: string[] = []; + + let resolveResponse: () => void; + const responseReceived = new Promise((resolve) => { + resolveResponse = resolve; + }); + let resolveRemoteSynced: () => void; + const remoteSynced = new Promise((resolve) => { + resolveRemoteSynced = resolve; + }); + let resolveRemoteBarrier: () => void; + const remoteBarrierReceived = new Promise((resolve) => { + resolveRemoteBarrier = resolve; + }); + let resolveSenderSynced: () => void; + const senderSynced = new Promise((resolve) => { + resolveSenderSynced = resolve; + }); + + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + async onStateless({ connection }) { + connection.sendStateless(response); + }, + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + prefix: redisPrefix, + }), + ], + async onStateless() { + t.fail(); + }, + }); + + newHocuspocusProvider(t, anotherServer, { + onSynced() { + resolveRemoteSynced(); + }, + onStateless({ payload }) { + if (payload === barrier) { + resolveRemoteBarrier(); + return; + } + remotePayloads.push(payload); + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + resolveSenderSynced(); + }, + onStateless({ payload }) { + if (payload === response) { + resolveResponse(); + } + }, + }); + + await Promise.all([remoteSynced, senderSynced]); + provider.sendStateless("ok"); + await responseReceived; + + const document = server.documents.get("hocuspocus-test"); + if (!document) { + throw new Error("Expected the document to be loaded"); + } + // Redis preserves publish order, so receiving this broadcast proves any + // accidental propagation of the earlier response has already arrived. + document.broadcastStateless(barrier); + await remoteBarrierReceived; + + t.deepEqual(remotePayloads, []); +}); diff --git a/tests/extension-redis/onStoreDocument.ts b/tests/extension-redis/onStoreDocument.ts new file mode 100644 index 0000000..85f4334 --- /dev/null +++ b/tests/extension-redis/onStoreDocument.ts @@ -0,0 +1,155 @@ +import test from 'ava' +import type { onStoreDocumentPayload } from '@hocuspocus/server' +import { Redis } from '@hocuspocus/extension-redis' +import type { HocuspocusProvider } from '@hocuspocus/provider' +import { uuidv4 } from 'lib0/random' +import { newHocuspocus, newHocuspocusProvider, redisConnectionSettings } from '../utils/index.ts' + +test('stores documents without conflicts', async t => { + await new Promise(async resolve => { + // eslint-disable-next-line prefer-const + let anotherProvider: HocuspocusProvider + + class CustomStorageExtension { + async onStoreDocument({ document }: onStoreDocumentPayload) { + t.is(document.getArray('foo').get(0), 'bar') + t.is(document.getArray('foo').get(0), anotherProvider.document.getArray('foo').get(0)) + + resolve('done') + } + } + + const server = await newHocuspocus(t, { + name: 'redis-1', + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${uuidv4()}`, + prefix: 'extension-redis/onStoreDocument1', + }), + new CustomStorageExtension(), + ], + }) + + const anotherServer = await newHocuspocus(t, { + name: 'redis-2', + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${uuidv4()}`, + prefix: 'extension-redis/onStoreDocument1', + }), + new CustomStorageExtension(), + ], + }) + + newHocuspocusProvider(t, server) + + anotherProvider = newHocuspocusProvider(t, anotherServer, { + onSynced() { + // once we're setup make an edit on anotherProvider, if all succeeds the onStoreDocument + // callback will be called after the debounce period and all docs will + // be identical + anotherProvider.document.getArray('foo').insert(0, ['bar']) + anotherProvider.disconnect() + }, + }) + }) +}) + +test('stores documents when the last client disconnects', async t => { + await new Promise(async resolve => { + // eslint-disable-next-line prefer-const + let provider: HocuspocusProvider + + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + prefix: 'extension-redis/onStoreDocument2', + ...redisConnectionSettings, + }), + ], + onStoreDocument: async ({ document }) => { + t.is( + provider.document.getArray('foo').get(0), + document.getArray('foo').get(0), + ) + + resolve('done') + }, + }) + + provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + provider.disconnect() + }, + }) + }) +}) + +test('document gets unloaded on both servers after disconnection', async t => { + await new Promise(async resolve => { + class CustomStorageExtension { + priority = 10 + + onStoreDocument({ document }: onStoreDocumentPayload) { + console.log('storing') + return new Promise(resolve2 => { + setTimeout(() => { + console.log('stored') + + resolve2('') + }, 3000) + }) + } + } + + const server = await newHocuspocus(t, { + name: 'redis-1', + extensions: [ + new Redis({ + ...redisConnectionSettings, + prefix: 'extension-redis/onStoreDocument3', + }), + new CustomStorageExtension(), + ], + }) + + const anotherServer = await newHocuspocus(t, { + name: 'redis-2', + extensions: [ + new Redis({ + ...redisConnectionSettings, + prefix: 'extension-redis/onStoreDocument3', + }), + new CustomStorageExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server) + + const anotherProvider = newHocuspocusProvider(t, anotherServer, { + onSynced() { + // once we're setup make an edit on anotherProvider, if all succeeds the onStoreDocument + // callback will be called after the debounce period and all docs will + // be identical + anotherProvider.document.getArray('foo').insert(0, ['bar']) + provider.document.getArray('foo2').insert(0, ['bar']) + + setTimeout(() => { + provider.configuration.websocketProvider.disconnect() + anotherProvider.configuration.websocketProvider.disconnect() + + setTimeout(() => { + t.is(anotherServer.documents.size, 0) + t.is(server.documents.size, 0) + + resolve('') + }, 10000) // must be higher than CustomStorageExtension delay (3s) + RedisExtension.disconnectDelay (1s) + }, 1500) + + }, + }) + }) +}) diff --git a/tests/extension-redis/openDirectConnection.ts b/tests/extension-redis/openDirectConnection.ts new file mode 100644 index 0000000..af8305d --- /dev/null +++ b/tests/extension-redis/openDirectConnection.ts @@ -0,0 +1,74 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import test from "ava"; +import { newHocuspocus, redisConnectionSettings, sleep } from "../utils/index.ts"; + +const newRedisServer = (t: any, prefix: string, identifier: string, options = {}) => + newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `${identifier}${crypto.randomUUID()}`, + prefix, + // Keep teardown quick — these delays are unrelated to load-time sync. + disconnectDelay: 100, + ...options, + }), + ], + }); + +test("direct connection on a second instance sees edits that only live in another instance's memory", async (t) => { + const prefix = `extension-redis/openDirectConnection-${crypto.randomUUID()}`; + + const serverA = await newRedisServer(t, prefix, "serverA"); + const serverB = await newRedisServer(t, prefix, "serverB"); + + // Open + modify the document on server A and keep the connection open so the + // document stays in memory on A and is never persisted to storage. The only + // way server B can learn about this edit is via the Redis sync handshake. + const directA = await serverA.openDirectConnection("sync-on-open"); + await directA.transact((doc) => { + doc.getMap("config").set("foo", "bar"); + }); + + // Open a direct connection on server B and read immediately. Without an + // inbound-sync wait in afterLoadDocument, B only has the (empty) state it + // loaded from storage, so this read sees `undefined`. + const directB = await serverB.openDirectConnection("sync-on-open"); + + let seenValue: unknown; + await directB.transact((doc) => { + seenValue = doc.getMap("config").get("foo"); + }); + + t.is(seenValue, "bar"); + + await directB.disconnect(); + await directA.disconnect(); +}); + +test("openDirectConnection on a single instance does not stall waiting for absent peers", async (t) => { + const prefix = `extension-redis/openDirectConnection-solo-${crypto.randomUUID()}`; + + // A generous sync timeout: if presence detection is broken and we wait for + // peers that don't exist, this load would take ~5s instead of being instant. + const server = await newRedisServer(t, prefix, "solo", { + awaitInitialSyncTimeout: 5000, + }); + + const start = Date.now(); + const direct = await server.openDirectConnection("solo-doc"); + const elapsed = Date.now() - start; + + t.true( + elapsed < 2000, + `openDirectConnection should not block on absent peers (took ${elapsed}ms)`, + ); + + await direct.transact((doc) => { + doc.getMap("config").set("foo", "bar"); + }); + t.is(direct.document?.getMap("config").get("foo"), "bar"); + + await direct.disconnect(); + await sleep(50); +}); diff --git a/tests/extension-redis/publishCoalescing.ts b/tests/extension-redis/publishCoalescing.ts new file mode 100644 index 0000000..40ea698 --- /dev/null +++ b/tests/extension-redis/publishCoalescing.ts @@ -0,0 +1,205 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import { Document, IncomingMessage, MessageType } from "@hocuspocus/server"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + redisConnectionSettings, + sleep, +} from "../utils/index.ts"; +import { retryableAssertion } from "../utils/retryableAssertion.ts"; + +/** + * Counts what actually reaches Redis by wrapping the extension's publisher. + */ +const countingRedis = () => { + const extension = new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }); + + const publisher = (extension as any).pub; + const original = publisher.publish.bind(publisher); + let published = 0; + + publisher.publish = (...args: any[]) => { + published += 1; + + return original(...args); + }; + + return { extension, publishCount: () => published, reset: () => (published = 0) }; +}; + +test("coalesces a burst of changes into a single publish", async (t) => { + const redis = countingRedis(); + const server = await newHocuspocus(t, { extensions: [redis.extension] }); + + const provider = newHocuspocusProvider(t, server, { + name: "redis-coalescing", + awareness: null, + }); + + await new Promise((resolve) => provider.on("synced", () => resolve("done"))); + await sleep(300); + redis.reset(); + + // Drive the burst on the server's own document. Twenty separate transactions + // applied synchronously are guaranteed to share one event loop turn; sending + // them through the provider would leave frame delivery, and therefore the + // turn boundaries, outside the test's control. + const document = server.documents.get("redis-coalescing"); + + t.truthy(document); + + const map = document!.getMap("test"); + for (let i = 0; i < 20; i += 1) { + map.set(`key-${i}`, i); + } + + await sleep(300); + + // Without coalescing this is one SyncStep1 publish per change, so 20. + t.is(redis.publishCount(), 1); +}); + +test("a failing sync publish does not swallow the awareness publish", async (t) => { + const extension = new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }); + + const server = await newHocuspocus(t, { extensions: [extension] }); + const provider = newHocuspocusProvider(t, server, { + name: "redis-publish-failure", + }); + + await new Promise((resolve) => provider.on("synced", () => resolve("done"))); + await sleep(300); + + // Reject the sync step specifically, by message type rather than by call + // order, and let everything else publish normally. + const publisher = (extension as any).pub; + const original = publisher.publish.bind(publisher); + const attempted: number[] = []; + const succeeded: number[] = []; + + publisher.publish = (key: string, payload: Buffer) => { + const [, messageBuffer] = (extension as any).decodeMessage(payload); + const message = new IncomingMessage(messageBuffer); + + message.readVarString(); // document name + + const messageType = message.readVarUint(); + + attempted.push(messageType); + + if (messageType === MessageType.Sync) { + return Promise.reject(new Error("redis is down")); + } + + succeeded.push(messageType); + + return original(key, payload); + }; + + // Both applied server-side and synchronously, so they share one turn and + // therefore one pending entry — which is the case under test. + const document = server.documents.get("redis-publish-failure"); + + t.truthy(document); + + document!.getMap("test").set("a", 1); + document!.awareness.setLocalState({ user: "jan" }); + + await sleep(300); + + t.true(attempted.includes(MessageType.Sync), "the sync step was attempted"); + t.true( + succeeded.includes(MessageType.Awareness), + "the awareness update was published despite the sync step failing", + ); +}); + +test("a reloaded document does not inherit the previous instance's pending publish", async (t) => { + const extension = new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }); + + const server = await newHocuspocus(t, { extensions: [extension] }); + const provider = newHocuspocusProvider(t, server, { + name: "redis-reload", + awareness: null, + }); + + await new Promise((resolve) => provider.on("synced", () => resolve("done"))); + + const first = server.documents.get("redis-reload"); + + t.truthy(first); + + // A fast reconnect can replace the Document under an unchanged name while a + // publish is still buffered, because `afterUnloadDocument` returns early and + // never clears it. Buffering against the old instance and then scheduling + // against a new one must not carry the stale document forward. + const schedule = (extension as any).schedulePublish.bind(extension); + const pending = (extension as any).pendingPublishes; + + schedule("redis-reload", first); + t.is(pending.get("redis-reload").document, first); + + const reloaded = new Document("redis-reload"); + + t.is(schedule("redis-reload", reloaded).document, reloaded); + t.is(pending.get("redis-reload").document, reloaded); + + reloaded.destroy(); +}); + +test("a coalesced burst still reaches another instance", async (t) => { + const server = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `server${crypto.randomUUID()}`, + }), + ], + }); + + const anotherServer = await newHocuspocus(t, { + extensions: [ + new Redis({ + ...redisConnectionSettings, + identifier: `anotherServer${crypto.randomUUID()}`, + }), + ], + }); + + const provider = newHocuspocusProvider(t, server, { + name: "redis-coalescing-sync", + awareness: null, + }); + const anotherProvider = newHocuspocusProvider(t, anotherServer, { + name: "redis-coalescing-sync", + awareness: null, + }); + + await new Promise((resolve) => + anotherProvider.on("synced", () => resolve("done")), + ); + + const map = provider.document.getMap("test"); + for (let i = 0; i < 20; i += 1) { + map.set(`key-${i}`, i); + } + + // Every change of the collapsed burst has to arrive, not just the last one. + await retryableAssertion(t, (tt) => { + const received = anotherProvider.document.getMap("test"); + + for (let i = 0; i < 20; i += 1) { + tt.is(received.get(`key-${i}`), i); + } + }); +}); diff --git a/tests/extension-redis/syncStep2Routing.ts b/tests/extension-redis/syncStep2Routing.ts new file mode 100644 index 0000000..91388cb --- /dev/null +++ b/tests/extension-redis/syncStep2Routing.ts @@ -0,0 +1,173 @@ +import { Redis } from "@hocuspocus/extension-redis"; +import { IncomingMessage, MessageType } from "@hocuspocus/server"; +import test from "ava"; +import { + newHocuspocus, + redisConnectionSettings, + sleep, +} from "../utils/index.ts"; + +const newRedisServer = async (t: any, prefix: string, identifier: string) => { + const extension = new Redis({ + ...redisConnectionSettings, + identifier: `${identifier}-${crypto.randomUUID()}`, + prefix, + disconnectDelay: 100, + }); + + const server = await newHocuspocus(t, { extensions: [extension] }); + + return { server, extension }; +}; + +/** Simulate lost pub/sub delivery: stop feeding inbound messages to the extension. */ +const silence = (extension: Redis) => { + const sub = (extension as any).sub; + const handler = (extension as any).handleIncomingMessage; + sub.removeListener("messageBuffer", handler); + + return () => sub.on("messageBuffer", handler); +}; + +test("a SyncStep2 computed for another instance must not truncate a stale bystander", async (t) => { + const prefix = `extension-redis/crossInstanceSyncStep2-${crypto.randomUUID()}`; + const docName = "poison"; + + const a = await newRedisServer(t, prefix, "serverA"); + const b = await newRedisServer(t, prefix, "serverB"); + const c = await newRedisServer(t, prefix, "serverC"); + + const directA = await a.server.openDirectConnection(docName); + const directB = await b.server.openDirectConnection(docName); + const directC = await c.server.openDirectConnection(docName); + + // All three instances hold the document and agree on "OLD". + await directA.transact((doc) => { + doc.getText("t").insert(0, "OLD"); + }); + await sleep(500); + + t.is(directA.document!.getText("t").toString(), "OLD", "A starts at OLD"); + t.is(directB.document!.getText("t").toString(), "OLD", "B starts at OLD"); + t.is(directC.document!.getText("t").toString(), "OLD", "C starts at OLD"); + + // A misses the rewrite (Redis pub/sub is fire-and-forget, so a dropped + // message, a reconnect, or a busy process all produce this). + const unsilence = silence(a.extension); + + await directB.transact((doc) => { + const text = doc.getText("t"); + text.delete(0, text.length); + text.insert(0, "NEW"); + }); + await sleep(500); + + t.is(directB.document!.getText("t").toString(), "NEW", "B rewrote to NEW"); + t.is(directC.document!.getText("t").toString(), "NEW", "C received NEW"); + t.is(directA.document!.getText("t").toString(), "OLD", "A is stale at OLD"); + + unsilence(); + + // Record every state A's document passes through from here on. + const seen: string[] = []; + directA.document!.on("update", () => { + seen.push(directA.document!.getText("t").toString()); + }); + + // Any change on C publishes a SyncStep1 with C's (up-to-date) state vector. + // B answers it with SyncStep2 = encodeStateAsUpdate(B, C.sv) — zero structs + // plus the full delete set — on the shared document channel, where A also + // hears it. + await directC.transact((doc) => { + doc.getMap("noise").set("k", 1); + }); + await sleep(500); + + t.log("states A passed through:", JSON.stringify(seen)); + t.log("A final:", JSON.stringify(directA.document!.getText("t").toString())); + + t.false( + seen.includes(""), + "A must never observe an empty document: it would broadcast that to its own clients and can persist it", + ); + + await directA.disconnect(); + await directB.disconnect(); + await directC.disconnect(); +}); + +/** `messageYjsSyncStep2` from y-protocols/sync, which is not resolvable here. */ +const SYNC_STEP_2 = 1; + +test("sync replies are published to the requester's reply channel only", async (t) => { + const prefix = `extension-redis/syncStep2Routing-channel-${crypto.randomUUID()}`; + const docName = "routing"; + + const identifierA = `serverA-${crypto.randomUUID()}`; + const extensionA = new Redis({ + ...redisConnectionSettings, + identifier: identifierA, + prefix, + disconnectDelay: 100, + }); + const serverA = await newHocuspocus(t, { extensions: [extensionA] }); + + const b = await newRedisServer(t, prefix, "serverB"); + + const published: Array<{ channel: string; syncType?: number }> = []; + const publisher = (b.extension as any).pub; + const original = publisher.publish.bind(publisher); + + publisher.publish = (channel: string, payload: Buffer) => { + const [, messageBuffer] = (b.extension as any).decodeMessage(payload); + const message = new IncomingMessage(messageBuffer); + + message.readVarString(); // document name + + const type = message.readVarUint(); + + published.push({ + channel, + syncType: + type === MessageType.Sync || type === MessageType.SyncReply + ? message.readVarUint() + : undefined, + }); + + return original(channel, payload); + }; + + // B holds the document with state that only lives in its memory. + const directB = await b.server.openDirectConnection(docName); + await directB.transact((doc) => { + doc.getText("t").insert(0, "hello"); + }); + await sleep(300); + published.length = 0; + + // A loads the same document, so B has to answer A's SyncStep1. + const directA = await serverA.openDirectConnection(docName); + await sleep(300); + + t.is(directA.document!.getText("t").toString(), "hello", "A synced from B"); + + const replies = published.filter((entry) => entry.syncType === SYNC_STEP_2); + + t.true(replies.length > 0, "B answered with its state"); + + for (const reply of replies) { + t.is(reply.channel, `${prefix}#reply:${identifierA}`); + } + + t.false( + published.some( + (entry) => + entry.channel === `${prefix}:${docName}` && + entry.syncType === SYNC_STEP_2, + ), + "no state is published on the shared document channel", + ); + + await directA.disconnect(); + await directB.disconnect(); +}); diff --git a/tests/extension-s3/fetch.ts b/tests/extension-s3/fetch.ts new file mode 100644 index 0000000..1cc9f71 --- /dev/null +++ b/tests/extension-s3/fetch.ts @@ -0,0 +1,153 @@ +// @ts-nocheck - AWS SDK types resolved through extension package +import test from "ava"; +import sinon from "sinon"; +import { S3 } from "@hocuspocus/extension-s3"; +import { + S3Client, + CreateBucketCommand, + HeadBucketCommand, + GetObjectCommand, + PutObjectCommand, + HeadObjectCommand, +} from "@aws-sdk/client-s3"; +import { ReadableStream } from "stream/web"; +import * as Y from 'yjs' + +const testConfig = { + bucket: "hocuspocus-test", + prefix: "test-documents/", +}; + +const storage = new Map(); +const sandbox = sinon.createSandbox(); +// Create a single dummy client to be used in all tests +const dummyS3Client = new S3Client({}); + +test.beforeEach(() => { + sandbox.restore(); + storage.clear(); + + sandbox.stub(S3Client.prototype, "send").callsFake(async (command: any) => { + if (command instanceof HeadBucketCommand || command instanceof HeadObjectCommand) { + return Promise.resolve(); + } + if (command instanceof CreateBucketCommand) { + return Promise.resolve(); + } + if (command instanceof GetObjectCommand) { + const key = command.input.Key as string; // 确保 key 是 string 类型 + if (storage.has(key)) { + const buffer = storage.get(key)!; + return { + Body: { + // Mimic AWS SDK v3 GetObjectCommand Body interface used in the extension + transformToWebStream: () => + new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(buffer)); + controller.close(); + }, + }), + }, + }; + } + const err = new Error("NoSuchKey"); + err.name = "NoSuchKey"; + // @ts-expect-error + err.$metadata = { httpStatusCode: 404 }; + throw err; + } + if (command instanceof PutObjectCommand) { + const key = command.input.Key as string; // 确保 key 是 string 类型 + const body = command.input.Body; + if (body instanceof Buffer) { + storage.set(key, body); + } else { + throw new Error("Body must be a Buffer"); + } + return Promise.resolve(); + } + }); +}); + +test.afterEach.always(() => { + sandbox.restore(); + storage.clear(); +}); + +test("should throw an error without bucket name", async (t) => { + t.throws( + () => { + new S3({}); + }, + { message: "S3 bucket name is required" } + ); +}); + +test("should create extension with valid configuration", async (t) => { + const extension = new S3({ ...testConfig, s3Client: dummyS3Client }); + t.truthy(extension); + t.is(extension.configuration.bucket, testConfig.bucket); +}); + +test("should store and retrieve documents", async (t) => { + const extension = new S3({ ...testConfig, s3Client: dummyS3Client }); + await (extension as any).onConfigure(); + + const documentName = "test-document"; + + const doc = new Y.Doc(); + doc.getMap('test').set('key', 'value'); + + await extension.configuration.store({ + documentName, + state: Buffer.from(Y.encodeStateAsUpdate(doc)), + } as any); + + const fetched = await extension.configuration.fetch({ documentName } as any); + t.truthy(fetched); + t.true(fetched instanceof Uint8Array); + t.true((fetched as Uint8Array).length > 0); + + const doc2 = new Y.Doc(); + Y.applyUpdate(doc2, fetched!); + t.is(doc2.getMap('test').get('key'), 'value'); +}); + +test("should handle non-existent documents", async (t) => { + const extension = new S3({ ...testConfig, s3Client: dummyS3Client }); + await (extension as any).onConfigure(); + + const result = await extension.configuration.fetch({ + documentName: "non-existent-document", + } as any); + + t.is(result, null); +}); + +test("should work with custom S3 client", async (t) => { + const extension = new S3({ + bucket: testConfig.bucket, + s3Client: dummyS3Client, + }); + + t.truthy(extension); + t.is(extension.configuration.bucket, testConfig.bucket); +}); + +test("should use correct object key format", async (t) => { + const documentName = "test-document-key-format"; + const extension = new S3({ ...testConfig, s3Client: dummyS3Client }); + await (extension as any).onConfigure(); + + const doc = new Y.Doc(); + doc.getMap('test').set('key', 'value'); + + await extension.configuration.store({ + documentName, + state: Buffer.from(Y.encodeStateAsUpdate(doc)), + } as any); + + const expectedKey = `${testConfig.prefix}${documentName}.bin`; + t.true(storage.has(expectedKey), `Storage should have key: ${expectedKey}`); +}); diff --git a/tests/extension-throttle/banning.ts b/tests/extension-throttle/banning.ts new file mode 100644 index 0000000..4237921 --- /dev/null +++ b/tests/extension-throttle/banning.ts @@ -0,0 +1,103 @@ +import test from 'ava' +import type { onConnectPayload } from '@hocuspocus/server' +import * as MockDate from 'mockdate' +import { Throttle } from '@hocuspocus/extension-throttle' + +const getOnConnectPayload = (ip: string) => { + return { + request: { + headers: new Headers({ + 'x-real-ip': ip, + }), + }, + } as unknown as onConnectPayload +} +const generateRequests = async (instance: Throttle, ip: string, numberOfRequests: number) => { + for (let i = 0; i < numberOfRequests; i += 1) { + // eslint-disable-next-line no-await-in-loop + await instance.onConnect(getOnConnectPayload(ip)) + } +} + +test('throttle extension bans properly', async t => { + const throttle = new Throttle({ banTime: 5, throttle: 15 }) + t.teardown(() => throttle.onDestroy()) + const ip = '127.0.0.1' + + t.false(throttle.isBanned(ip)) + + await generateRequests(throttle, ip, 15) + + try { + await throttle.onConnect(getOnConnectPayload(ip)) + t.fail() + } catch (e) { + t.true(throttle.isBanned(ip)) + } + +}) + +test('throttle extension unbans properly', async t => { + const throttle = new Throttle({ banTime: 5, throttle: 15 }) + t.teardown(() => throttle.onDestroy()) + const ip = '127.0.0.1' + + t.false(throttle.isBanned(ip)) + + await generateRequests(throttle, ip, 15) + + try { + await throttle.onConnect(getOnConnectPayload(ip)) + t.fail() + } catch (e) { + t.true(throttle.isBanned(ip)) + } + + MockDate.set(Date.now() + 1000 * (throttle.configuration.banTime * 60)) + + await throttle.onConnect(getOnConnectPayload(ip)) + t.false(throttle.isBanned(ip)) + + MockDate.reset() +}) + +test.serial('map cleanup works for connectionsByIp', async t => { + const throttle = new Throttle({ consideredSeconds: 60 }) + t.teardown(() => throttle.onDestroy()) + const ip = '127.0.0.1' + + await generateRequests(throttle, ip, 10) + + t.is(throttle.connectionsByIp.get(ip)!.length, 10) + + MockDate.set(Date.now() + 1000 * throttle.configuration.consideredSeconds) + + await throttle.clearMaps() + + t.false(throttle.connectionsByIp.has(ip)) + + MockDate.reset() +}) + +test.serial('map cleanup works for bannedIps', async t => { + const throttle = new Throttle({ consideredSeconds: 60, throttle: 15 }) + t.teardown(() => throttle.onDestroy()) + const ip = '127.0.0.1' + + await generateRequests(throttle, ip, 15) + + try { + await throttle.onConnect(getOnConnectPayload(ip)) + // eslint-disable-next-line no-empty + } catch (e) {} + + t.true(throttle.bannedIps.has(ip)) + + MockDate.set(Date.now() + 1000 * throttle.configuration.banTime * 60) + + await throttle.clearMaps() + + t.false(throttle.bannedIps.has(ip)) + + MockDate.reset() +}) diff --git a/tests/extension-throttle/configuration.ts b/tests/extension-throttle/configuration.ts new file mode 100644 index 0000000..47cf433 --- /dev/null +++ b/tests/extension-throttle/configuration.ts @@ -0,0 +1,26 @@ +import test from 'ava' +import { Throttle } from '@hocuspocus/extension-throttle' + +test('throttle has the default configuration', async t => { + const throttle = new Throttle() + t.teardown(() => throttle.onDestroy()) + t.is(throttle.configuration.throttle, 15) +}) + +test('banTime has the default configuration', async t => { + const throttle = new Throttle() + t.teardown(() => throttle.onDestroy()) + t.is(throttle.configuration.banTime, 5) +}) + +test('throttle has a custom value', async t => { + const throttle = new Throttle({ throttle: 100 }) + t.teardown(() => throttle.onDestroy()) + t.is(throttle.configuration.throttle, 100) +}) + +test('banTime has a custom value', async t => { + const throttle = new Throttle({ banTime: 100 }) + t.teardown(() => throttle.onDestroy()) + t.is(throttle.configuration.banTime, 100) +}) diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..03fa6ca --- /dev/null +++ b/tests/package.json @@ -0,0 +1,28 @@ +{ + "name": "tests", + "version": "3.4.6-rc.1", + "private": true, + "type": "module", + "dependencies": { + "@aws-sdk/client-s3": "^3.990.0", + "@hocuspocus/common": "workspace:^", + "@hocuspocus/extension-database": "workspace:^", + "@hocuspocus/extension-logger": "workspace:^", + "@hocuspocus/extension-redis": "workspace:^", + "@hocuspocus/extension-s3": "workspace:^", + "@hocuspocus/extension-throttle": "workspace:^", + "@hocuspocus/provider": "workspace:^", + "@hocuspocus/server": "workspace:^", + "@hocuspocus/transformer": "workspace:^", + "redis": "^4.7.1", + "sinon": "^12.0.1", + "ws": "^8.21.0", + "yjs": "^13.6.29" + }, + "devDependencies": { + "@types/redis": "^4.0.11", + "@types/sinon": "^10.0.20", + "lib0": "^0.2.117", + "mockdate": "^3.0.5" + } +} diff --git a/tests/provider/awarenessEcho.ts b/tests/provider/awarenessEcho.ts new file mode 100644 index 0000000..a1a7366 --- /dev/null +++ b/tests/provider/awarenessEcho.ts @@ -0,0 +1,79 @@ +import test from 'ava' +import type { HocuspocusProvider } from '@hocuspocus/provider' +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +const AWARENESS_MESSAGE = 'Awareness states update' + +const hasAwarenessField = (provider: HocuspocusProvider) => + Array.from(provider.awareness?.getStates().values() ?? []).some(state => state.foo === 'bar') + +test('does not echo awareness received from the server back to it', async t => { + const server = await newHocuspocus(t) + + const first = newHocuspocusProvider(t, server, { name: 'awareness-echo' }) + + let outgoingAwarenessMessages = 0 + const second = newHocuspocusProvider(t, server, { + name: 'awareness-echo', + onOutgoingMessage({ message }) { + if (message.description === AWARENESS_MESSAGE) { + outgoingAwarenessMessages += 1 + } + }, + }) + + await new Promise(resolve => second.on('synced', () => resolve('done'))) + + // Ignore whatever the handshake produced; only the reaction to a remote + // change is under test here. + await sleep(100) + outgoingAwarenessMessages = 0 + + first.setAwarenessField('foo', 'bar') + + // The remote state does arrive, so the assertion below is not vacuous. + await retryableAssertion(t, tt => { + tt.true(hasAwarenessField(second)) + }) + + await sleep(100) + + t.is(outgoingAwarenessMessages, 0) +}) + +test('still sends awareness changes that originate locally', async t => { + const server = await newHocuspocus(t) + + const first = newHocuspocusProvider(t, server, { name: 'awareness-local' }) + const second = newHocuspocusProvider(t, server, { name: 'awareness-local' }) + + await new Promise(resolve => second.on('synced', () => resolve('done'))) + + second.setAwarenessField('foo', 'bar') + + await retryableAssertion(t, tt => { + tt.true(hasAwarenessField(first)) + }) +}) + +test('still tells the server when a client goes away', async t => { + const server = await newHocuspocus(t) + + const first = newHocuspocusProvider(t, server, { name: 'awareness-destroy' }) + const second = newHocuspocusProvider(t, server, { name: 'awareness-destroy' }) + + await new Promise(resolve => second.on('synced', () => resolve('done'))) + + second.setAwarenessField('foo', 'bar') + + await retryableAssertion(t, tt => { + tt.true(hasAwarenessField(first)) + }) + + second.destroy() + + await retryableAssertion(t, tt => { + tt.false(hasAwarenessField(first)) + }) +}) diff --git a/tests/provider/flushDelay.ts b/tests/provider/flushDelay.ts new file mode 100644 index 0000000..20350cc --- /dev/null +++ b/tests/provider/flushDelay.ts @@ -0,0 +1,138 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('batches rapid document updates into a single outgoing message', async t => { + const server = await newHocuspocus(t) + let updateMessages = 0 + + await new Promise(resolve => { + const provider = newHocuspocusProvider(t, server, { + awareness: null, + flushDelay: 100, + onOutgoingMessage({ message }) { + if (message.description === 'A document update') { + updateMessages += 1 + } + }, + async onSynced() { + provider.document.getMap('test').set('a', 1) + provider.document.getMap('test').set('b', 2) + provider.document.getMap('test').set('c', 3) + + // Still inside the flush window: nothing has been sent yet. + t.is(updateMessages, 0) + + await sleep(300) + + // The three updates were merged into a single message. + t.is(updateMessages, 1) + resolve('done') + }, + }) + }) +}) + +test('a merged batch still converges on another client', async t => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + awareness: null, + flushDelay: 100, + }) + + await new Promise(resolve => { + provider.on('synced', () => resolve('done')) + }) + + provider.document.getMap('test').set('a', 1) + provider.document.getMap('test').set('b', 2) + provider.document.getMap('test').set('c', 3) + + const reader = newHocuspocusProvider(t, server, { awareness: null }) + + await retryableAssertion(t, tt => { + const map = reader.document.getMap('test') + tt.is(map.get('a'), 1) + tt.is(map.get('b'), 2) + tt.is(map.get('c'), 3) + }) +}) + +test('sends one message per update when flushDelay is disabled', async t => { + const server = await newHocuspocus(t) + let updateMessages = 0 + + await new Promise(resolve => { + const provider = newHocuspocusProvider(t, server, { + awareness: null, + onOutgoingMessage({ message }) { + if (message.description === 'A document update') { + updateMessages += 1 + } + }, + async onSynced() { + provider.document.getMap('test').set('a', 1) + provider.document.getMap('test').set('b', 2) + provider.document.getMap('test').set('c', 3) + + await sleep(100) + + t.is(updateMessages, 3) + resolve('done') + }, + }) + }) +}) + +test('reports unsynced changes during the batch window and clears after sync', async t => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + awareness: null, + flushDelay: 100, + }) + + await new Promise(resolve => { + provider.on('synced', () => resolve('done')) + }) + + provider.document.getMap('test').set('a', 1) + provider.document.getMap('test').set('b', 2) + + // Buffered but not yet sent — still counts as unsynced. + t.is(provider.hasUnsyncedChanges, true) + + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, false) + }) +}) + +test('collapses rapid awareness changes into a single outgoing message', async t => { + const server = await newHocuspocus(t) + let awarenessMessages = 0 + + await new Promise(resolve => { + const provider = newHocuspocusProvider(t, server, { + flushDelay: 100, + onOutgoingMessage({ message }) { + if (message.description === 'Awareness states update') { + awarenessMessages += 1 + } + }, + async onSynced() { + // Ignore any awareness sent as part of the initial sync. + awarenessMessages = 0 + + provider.setAwarenessField('cursor', 1) + provider.setAwarenessField('cursor', 2) + provider.setAwarenessField('cursor', 3) + + await sleep(300) + + t.is(awarenessMessages, 1) + resolve('done') + }, + }) + }) +}) diff --git a/tests/provider/hasUnsyncedChanges.ts b/tests/provider/hasUnsyncedChanges.ts new file mode 100644 index 0000000..649381a --- /dev/null +++ b/tests/provider/hasUnsyncedChanges.ts @@ -0,0 +1,187 @@ +import test from 'ava' +import * as Y from 'yjs' +import { retryableAssertion } from '../utils/retryableAssertion.ts' +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' + +test("initially doesn't have unsynced changes", async t => { + return new Promise(async resolve => { + + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + t.is(provider.hasUnsyncedChanges, false) + t.is(provider.synced, false) + + setTimeout(() => { + t.is(provider.hasUnsyncedChanges, false) + t.is(provider.synced, true) + + resolve() + }, 200) + }) +}) + +test('has unsynced changes when updating', async t => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + awareness: undefined, + }) + + provider.document.getMap('test').set('foo', 'bar') + t.is(provider.hasUnsyncedChanges, true) + + // changes are synced + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, false) + }) +}) + +test('has unsynced changes when in readonly mode', async t => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig }) { + connectionConfig.readOnly = true + }, + }) + + const provider = newHocuspocusProvider(t, server, { token: 'readonly' }) + + provider.document.getMap('test').set('foo', 'bar') + + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, true) + }) + + await sleep(100) + + // confirm that the changes are not synced later either + t.is(provider.hasUnsyncedChanges, true) +}) + +test('has no unsynced changes when in readonly mode and no changes', async t => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig }) { + connectionConfig.readOnly = true + }, + }) + + const provider = newHocuspocusProvider(t, server, { token: 'readonly' }) + + // first, unsyncedChanges is briefly set to true when we're waiting for the ack of the initial sync + await new Promise((resolve, reject) => { + provider.on('unsyncedChanges', () => { + provider.off('unsyncedChanges') + if (provider.hasUnsyncedChanges) { + resolve('done') + } else { + reject() + } + }) + }) + + // then, it should be set to false when the sync message is confirmed + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, false) + }) +}) + +test('has unsynced changes when in readonly mode and receiving external update', async t => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig, token }) { + if (token === 'readonly') { + connectionConfig.readOnly = true + } + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'readonly', + }) + + provider.document.getMap('test').set('foo', 'bar') + + t.is(provider.hasUnsyncedChanges, true) + + await sleep(100) + + t.is(provider.hasUnsyncedChanges, true) + + const provider2 = newHocuspocusProvider(t, server, { + token: 'full-access', + }) + + provider2.document.getMap('test2').set('foo', 'bar') + + t.is(provider2.hasUnsyncedChanges, true) + + await retryableAssertion(t, tt => { + tt.is(provider2.hasUnsyncedChanges, false) + }) + + t.is(provider.hasUnsyncedChanges, true) +}) + +test('has unsynced changes when in readonly mode and initial document has changed', async t => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig }) { + connectionConfig.readOnly = true + }, + }) + + const document = new Y.Doc() + document.getMap('test').set('foo', 'bar') + + const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' }) + + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, true) + }) + + await sleep(100) + + t.is(provider.hasUnsyncedChanges, true) +}) + +test('has unsynced changes when in readonly mode and initial document has changed (deletion)', async t => { + const document = new Y.Doc() + document.getMap('test').set('foo', 'bar') + const initialState = Y.encodeStateAsUpdate(document) + + const server = await newHocuspocus(t, { + async onLoadDocument() { + return initialState + }, + async onAuthenticate({ connectionConfig }) { + connectionConfig.readOnly = true + }, + }) + + document.getMap('test').delete('foo') + + const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' }) + + await retryableAssertion(t, tt => { + tt.is(provider.hasUnsyncedChanges, true) + }) + + await sleep(100) + + t.is(provider.hasUnsyncedChanges, true) +}) + +test('has no unsynced changes when in readonly mode and initial document has not changed', async t => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig }) { + connectionConfig.readOnly = true + }, + }) + + const document = new Y.Doc() + + const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' }) + + await sleep(200) + + t.is(provider.hasUnsyncedChanges, false) +}) diff --git a/tests/provider/observe.ts b/tests/provider/observe.ts new file mode 100644 index 0000000..d45e20a --- /dev/null +++ b/tests/provider/observe.ts @@ -0,0 +1,83 @@ +import test from 'ava' +import * as Y from 'yjs' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('observe is called just once', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observe is called … + type.observe((events, transaction) => { + count += 1 + }) + + // Insert something … + type.insert(1, 'a') + + await retryableAssertion(t, tt => { + tt.is(count, 1) + }) + +}) + +test('observe is called for every single change', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observe is called … + type.observe((events, transaction) => { + count += 1 + }) + + // Insert something … + type.insert(1, 'a') + type.insert(2, 'b') + type.insert(3, 'c') + + await retryableAssertion(t, tt => { + tt.is(count, 3) + }) +}) + +test('observe is called once for a single transaction', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observe is called … + type.observe((events, transaction) => { + count += 1 + }) + + // Insert something … + Y.transact(provider.document, () => { + type.insert(1, 'a') + type.insert(2, 'b') + type.insert(3, 'c') + }) + + await retryableAssertion(t, tt => { + tt.is(count, 1) + }) +}) diff --git a/tests/provider/observeDeep.ts b/tests/provider/observeDeep.ts new file mode 100644 index 0000000..92d45d9 --- /dev/null +++ b/tests/provider/observeDeep.ts @@ -0,0 +1,82 @@ +import test from 'ava' +import * as Y from 'yjs' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('observeDeep is called just once', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observeDeep is called … + type.observeDeep((events, transaction) => { + count += 1 + }) + + // Insert something … + type.insert(1, 'a') + + await retryableAssertion(t, tt => { + tt.is(count, 1) + }) +}) + +test('observeDeep is called for every single change', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observeDeep is called … + type.observeDeep((events, transaction) => { + count += 1 + }) + + // Insert something … + type.insert(1, 'a') + type.insert(2, 'b') + type.insert(3, 'c') + + await retryableAssertion(t, tt => { + tt.is(count, 3) + }) +}) + +test('observeDeep is called once for a single transaction', async t => { + let count = 0 + + const server = await newHocuspocus(t) + const provider = newHocuspocusProvider(t, server) + + const type = provider.document.get( + 'xmlText', + Y.XmlText, + ) as unknown as Y.XmlText + + // Count how often observeDeep is called … + type.observeDeep((events, transaction) => { + count += 1 + }) + + // Insert something … + Y.transact(provider.document, () => { + type.insert(1, 'a') + type.insert(2, 'b') + type.insert(3, 'c') + }) + + await retryableAssertion(t, tt => { + tt.is(count, 1) + }) +}) diff --git a/tests/provider/onAuthenticated.ts b/tests/provider/onAuthenticated.ts new file mode 100644 index 0000000..94fde35 --- /dev/null +++ b/tests/provider/onAuthenticated.ts @@ -0,0 +1,91 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onAuthenticated callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }) { + if (token !== 'SUPER-SECRET-TOKEN') { + throw new Error() + } + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onAuthenticated() { + t.is(provider.isAuthenticated, true) + t.is(provider.authorizedScope, 'read-write') + t.pass() + resolve('done') + }, + }) + }) +}) + +test('executes the onAuthenticated callback when token is provided as a function that returns a promise', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }) { + if (token !== 'SUPER-SECRET-TOKEN') { + throw new Error() + } + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: async () => Promise.resolve('SUPER-SECRET-TOKEN'), + onAuthenticated() { + t.is(provider.isAuthenticated, true) + t.is(provider.authorizedScope, 'read-write') + t.pass() + resolve('done') + }, + }) + }) +}) + +test('executes the onAuthenticated callback when token is provided as a function that returns a string', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }) { + if (token !== 'SUPER-SECRET-TOKEN') { + throw new Error() + } + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: () => 'SUPER-SECRET-TOKEN', + onAuthenticated() { + t.is(provider.isAuthenticated, true) + t.is(provider.authorizedScope, 'read-write') + t.pass() + resolve('done') + }, + }) + }) +}) + +test('sets correct scope for readonly', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token, connectionConfig }) { + if (token !== 'SUPER-SECRET-TOKEN') { + throw new Error() + } + connectionConfig.readOnly = true + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onAuthenticated() { + t.is(provider.isAuthenticated, true) + t.is(provider.authorizedScope, 'readonly') + t.pass() + resolve('done') + }, + }) + }) +}) diff --git a/tests/provider/onAuthenticationFailed.ts b/tests/provider/onAuthenticationFailed.ts new file mode 100644 index 0000000..0bf8be0 --- /dev/null +++ b/tests/provider/onAuthenticationFailed.ts @@ -0,0 +1,20 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onAuthenticationFailed callback', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + async onAuthenticate({ token }) { + throw new Error() + }, + }).then(server => { + newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onAuthenticationFailed() { + t.pass() + resolve('done') + }, + }) + }) + }) +}) diff --git a/tests/provider/onAuthenticationFailedRetry.ts b/tests/provider/onAuthenticationFailedRetry.ts new file mode 100644 index 0000000..fcb028b --- /dev/null +++ b/tests/provider/onAuthenticationFailedRetry.ts @@ -0,0 +1,90 @@ +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + newHocuspocusProviderWebsocket, + sleep, +} from "../utils/index.ts"; + +test("provider retries auth with token function after initial failure", async (t) => { + const docName = "superSecretDoc"; + const requiredToken = "SUPER-SECRET-TOKEN"; + + const server = await newHocuspocus(t, { + async onAuthenticate({ token, documentName }) { + if (documentName !== docName) { + throw new Error(); + } + + if (token !== requiredToken) { + throw new Error(); + } + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + let tokenCallCount = 0; + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + name: docName, + token: () => { + tokenCallCount++; + return tokenCallCount === 1 ? "wrongToken" : requiredToken; + }, + onAuthenticationFailed() { + provider.sendToken(); + provider.startSync(); + }, + }); + + await sleep(2000); + + t.is(tokenCallCount, 2); + t.is(provider.isAuthenticated, true); +}); + +test("second provider with same doc name succeeds after first fails auth", async (t) => { + const docName = "superSecretDoc"; + const requiredToken = "SUPER-SECRET-TOKEN"; + + const server = await newHocuspocus(t, { + async onAuthenticate({ token, documentName }) { + if (documentName !== docName) { + throw new Error(); + } + + if (token !== requiredToken) { + throw new Error(); + } + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const providerFail = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "wrongToken", + name: docName, + onAuthenticated() { + t.fail("providerFail should not authenticate"); + }, + }); + + await sleep(1000); + + const providerOK = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: requiredToken, + name: docName, + onAuthenticationFailed() { + t.fail("providerOK should not fail auth"); + }, + }); + + await sleep(1000); + + t.is(providerFail.isAuthenticated, false); + t.is(providerOK.isAuthenticated, true); +}); diff --git a/tests/provider/onAwarenessChange.ts b/tests/provider/onAwarenessChange.ts new file mode 100644 index 0000000..ac2c414 --- /dev/null +++ b/tests/provider/onAwarenessChange.ts @@ -0,0 +1,169 @@ +import test from "ava"; +import { newHocuspocus, newHocuspocusProvider, sleep } from "../utils/index.ts"; + +test("onAwarenessChange callback is executed", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField("foo", "bar"); + }, + onAwarenessChange: ({ states }) => { + if (resolved) return; + resolved = true; + + t.is(states.length, 1); + t.is(states[0].foo, "bar"); + + resolve("done"); + }, + }); + }); +}); + +test("onAwarenessChange callback is executed, even when no awareness fields are set", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, server, { + onAwarenessChange: ({ states }) => { + if (resolved) return; + resolved = true; + + t.is(states.length, 2); + + resolve("done"); + }, + }); + + const anotherProvider = newHocuspocusProvider(t, server, { + async onConnect() { + anotherProvider.setAwarenessField("foo", "bar"); + provider.configuration.websocketProvider.connect(); + }, + }); + }); + + t.pass(); +}); + +test("onAwarenessChange callback is executed on provider destroy", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, + server, + { + onConnect() { + provider.destroy(); + }, + onAwarenessChange: ({ states }) => { + if (resolved) return; + resolved = true; + + t.is(states.length, 0); + resolve("done"); + }, + }, + { + maxAttempts: 1, + }, + ); + }); +}); + +test("gets the current awareness states from the server", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, server); + const provider2 = newHocuspocusProvider(t, server, { + onAwarenessChange: ({ states }) => { + if (resolved) return; + const state = states.find((state) => state.foo === "bar"); + + if (state && state.foo === "bar") { + resolved = true; + t.pass(); + resolve("done"); + } + }, + }); + + provider.setAwarenessField("foo", "bar"); + }); +}); + +test("shares awareness state with other users", async (t) => { + await new Promise(async (resolve) => { + let resolved = false; + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField("name", "player1"); + }, + onAwarenessChange: ({ states }) => { + if (resolved) return; + const player2 = !!states.filter((state) => state.name === "player2") + .length; + + if (player2) { + resolved = true; + t.is(player2, true); + resolve("done"); + } + }, + }); + + const anotherProvider = newHocuspocusProvider(t, server, { + onConnect() { + anotherProvider.setAwarenessField("name", "player2"); + }, + onAwarenessChange: ({ states }) => { + if (resolved) return; + const player1 = !!states.filter((state) => state.name === "player1") + .length; + + if (player1) { + t.is(player1, true); + } + }, + }); + }); +}); + +test("does not share awareness state with users in other documents", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + newHocuspocusProvider(t, server, { + async onConnect() { + await sleep(100); + + t.pass(); + resolve("done"); + }, + onAwarenessChange: ({ states }) => { + const player2 = !!states.filter((state) => state.name === "player2") + .length; + + if (player2) { + t.fail("Awareness state leaked!"); + } + }, + }); + + const anotherProvider = newHocuspocusProvider(t, server, { + name: "completely-different-and-unrelated-document", + onConnect() { + anotherProvider.setAwarenessField("name", "player2"); + }, + }); + }); +}); diff --git a/tests/provider/onAwarenessUpdate.ts b/tests/provider/onAwarenessUpdate.ts new file mode 100644 index 0000000..d360180 --- /dev/null +++ b/tests/provider/onAwarenessUpdate.ts @@ -0,0 +1,131 @@ +import test from 'ava' +import { AwarenessError } from '@hocuspocus/provider' +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' + +test('onAwarenessUpdate callback is executed', async t => { + await new Promise(async resolve => { + let resolved = false + const server = await newHocuspocus(t, { }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + onAwarenessUpdate: ({ states }) => { + if (resolved) return + resolved = true + + t.is(states.length, 1) + t.is(states[0].foo, 'bar') + + resolve('done') + }, + }) + }) +}) + +test('shares awareness state with other users', async t => { + await new Promise(async resolve => { + let resolved = false + const server = await newHocuspocus(t, { }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('name', 'player1') + }, + onAwarenessUpdate: ({ states }) => { + if (resolved) return + const player2 = !!states.filter(state => state.name === 'player2').length + + if (player2) { + resolved = true + t.is(player2, true) + resolve('done') + } + }, + }) + + const anotherProvider = newHocuspocusProvider(t, server, { + onConnect() { + anotherProvider.setAwarenessField('name', 'player2') + }, + onAwarenessUpdate: ({ states }) => { + if (resolved) return + const player1 = !!states.filter(state => state.name === 'player1').length + + if (player1) { + t.is(player1, true) + } + }, + }) + }) +}) + +test('does not share awareness state with users in other documents', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { }) + + newHocuspocusProvider(t, server, { + async onConnect() { + await sleep(100) + + t.pass() + resolve('done') + }, + onAwarenessUpdate: ({ states }) => { + const player2 = !!states.filter(state => state.name === 'player2').length + + if (player2) { + throw new Error('Awareness state leaked!') + } + }, + }) + + const anotherProvider = newHocuspocusProvider(t, server, { + name: 'hocuspocus-completely-different-and-unrelated-document', + onConnect() { + anotherProvider.setAwarenessField('name', 'player2') + }, + }) + }) +}) + +test('allows awareness to be null', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { }) + + newHocuspocusProvider(t, server, { + awareness: null, + async onConnect() { + await sleep(100) + + t.pass() + resolve('done') + }, + }) + }) +}) + +test('throws an error in setAwarenessFields if awareness is null', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + awareness: null, + onConnect() { + try { + provider.setAwarenessField('foo', 'bar') + t.fail() + } catch (err: any) { + if (err instanceof AwarenessError) { + t.pass() + } else { + t.fail() + } + } finally { + resolve('done') + } + }, + }) + }) +}) diff --git a/tests/provider/onClose.ts b/tests/provider/onClose.ts new file mode 100644 index 0000000..fb58226 --- /dev/null +++ b/tests/provider/onClose.ts @@ -0,0 +1,35 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('onClose callback is executed', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.configuration.websocketProvider.disconnect() + }, + onClose() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("on('close') callback is executed", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('connect', () => { + provider.configuration.websocketProvider.disconnect() + }) + + provider.on('close', () => { + t.pass() + resolve('done') + }) + }) +}) diff --git a/tests/provider/onConnect.ts b/tests/provider/onConnect.ts new file mode 100644 index 0000000..c3f775e --- /dev/null +++ b/tests/provider/onConnect.ts @@ -0,0 +1,28 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onConnect callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + newHocuspocusProvider(t, server, { + onConnect() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("executes the on('connect') callback", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('connect', () => { + t.pass() + resolve('done') + }) + }) +}) diff --git a/tests/provider/onDisconnect.ts b/tests/provider/onDisconnect.ts new file mode 100644 index 0000000..499f038 --- /dev/null +++ b/tests/provider/onDisconnect.ts @@ -0,0 +1,36 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('onDisconnect callback is executed', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + onDisconnect() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("on('disconnect') callback is executed", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('connect', () => { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }) + provider.on('disconnect', () => { + t.pass() + resolve('done') + }) + }) +}) diff --git a/tests/provider/onMessage.ts b/tests/provider/onMessage.ts new file mode 100644 index 0000000..b15e842 --- /dev/null +++ b/tests/provider/onMessage.ts @@ -0,0 +1,28 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onMessage callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { }) + + newHocuspocusProvider(t, server, { + onMessage() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("executes the on('message') callback", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('message', () => { + t.pass() + resolve('done') + }) + }) +}) diff --git a/tests/provider/onOpen.ts b/tests/provider/onOpen.ts new file mode 100644 index 0000000..18646d6 --- /dev/null +++ b/tests/provider/onOpen.ts @@ -0,0 +1,28 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('onOpen callback is executed', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + newHocuspocusProvider(t, server, { + onOpen() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("on('open') callback is executed", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('open', () => { + t.pass() + resolve('done') + }) + }) +}) diff --git a/tests/provider/onStateless.ts b/tests/provider/onStateless.ts new file mode 100644 index 0000000..acfc945 --- /dev/null +++ b/tests/provider/onStateless.ts @@ -0,0 +1,21 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onStateless callback', async t => { + const payloadToSend = 'STATELESS-MESSAGE' + await new Promise(async resolve => { + newHocuspocus(t, { + async onStateless({ payload }) { + t.is(payload, payloadToSend) + t.pass() + resolve('done') + }, + }).then(server => { + const provider = newHocuspocusProvider(t, server, { + onSynced: () => { + provider.sendStateless(payloadToSend) + }, + }) + }) + }) +}) diff --git a/tests/provider/onSynced.ts b/tests/provider/onSynced.ts new file mode 100644 index 0000000..71ff037 --- /dev/null +++ b/tests/provider/onSynced.ts @@ -0,0 +1,109 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' + +test('onSynced callback is executed', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + newHocuspocusProvider(t, server, { + onSynced() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test("on('synced') callback is executed", async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const provider = newHocuspocusProvider(t, server) + + provider.on('synced', () => { + t.pass() + resolve('done') + }) + }) +}) + +test('onSynced callback is executed, even when the onConnect takes longer', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect(data) { + await sleep(100) + }, + }) + + newHocuspocusProvider(t, server, { + onSynced() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test('onSynced callback is executed when the document is actually synced', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onLoadDocument({ document }) { + document.getArray('foo').insert(0, ['bar']) + + return document + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + const value = provider.document.getArray('foo').get(0) + t.is(value, 'bar') + + resolve('done') + }, + }) + }) +}) + +test('send all messages according to the protocol', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onLoadDocument({ document }) { + document.getArray('foo').insert(0, ['bar']) + + return document + }, + }) + + const provider = newHocuspocusProvider(t, server, { + async onSynced() { + t.deepEqual(provider.document.getArray('foo').get(0), 'bar') + + resolve('done') + }, + }) + }) +}) + +test('onSynced callback is executed when the document is actually synced, even if it takes longer', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onLoadDocument({ document }) { + await sleep(100) + + document.getArray('foo').insert(0, ['bar']) + + return document + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + const value = provider.document.getArray('foo').get(0) + t.is(value, 'bar') + + resolve('done') + }, + }) + }) +}) diff --git a/tests/providerwebsocket/configuration.ts b/tests/providerwebsocket/configuration.ts new file mode 100644 index 0000000..889747e --- /dev/null +++ b/tests/providerwebsocket/configuration.ts @@ -0,0 +1,18 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProviderWebsocket } from '../utils/index.ts' + +test('has default configuration (maxDelay = 30000)', async t => { + const server = await newHocuspocus(t) + const client = newHocuspocusProviderWebsocket(t, server) + + t.is(client.configuration.maxDelay, 30000) +}) + +test('overwrites the default configuration', async t => { + const server = await newHocuspocus(t) + const client = newHocuspocusProviderWebsocket(t, server, { + maxDelay: 10000, + }) + + t.is(client.configuration.maxDelay, 10000) +}) diff --git a/tests/providerwebsocket/maxAttempts.ts b/tests/providerwebsocket/maxAttempts.ts new file mode 100644 index 0000000..9a760cd --- /dev/null +++ b/tests/providerwebsocket/maxAttempts.ts @@ -0,0 +1,151 @@ +import { + HocuspocusProviderWebsocket, + type onMaxAttemptsFailedParameters, +} from "@hocuspocus/provider"; +import test from "ava"; +import { sleep } from "../utils/index.ts"; + +const exhaustedRetryOptions = { + url: "ws://127.0.0.1:1", + maxAttempts: 1, + delay: 1, + minDelay: 1, + initialDelay: 0, + jitter: false, +}; + +test("does not produce an unhandled rejection when maxAttempts are exhausted", async (t) => { + const unhandled: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandled.push(reason); + }; + process.on("unhandledRejection", onUnhandledRejection); + t.teardown(() => { + process.off("unhandledRejection", onUnhandledRejection); + }); + + const ws = new HocuspocusProviderWebsocket(exhaustedRetryOptions); + t.teardown(() => ws.destroy()); + + await sleep(300); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); + + t.is(unhandled.length, 0); +}); + +test("onMaxAttemptsFailed is executed when maxAttempts are exhausted", async (t) => { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("onMaxAttemptsFailed was not called")); + }, 2000); + + const ws = new HocuspocusProviderWebsocket({ + ...exhaustedRetryOptions, + onMaxAttemptsFailed({ error }) { + clearTimeout(timeout); + t.truthy(error); + ws.destroy(); + resolve("done"); + }, + }); + t.teardown(() => ws.destroy()); + }); +}); + +test("on('maxAttemptsFailed') is executed when maxAttempts are exhausted", async (t) => { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("maxAttemptsFailed was not emitted")); + }, 2000); + + const ws = new HocuspocusProviderWebsocket({ + ...exhaustedRetryOptions, + autoConnect: false, + }); + t.teardown(() => ws.destroy()); + + ws.on("maxAttemptsFailed", ({ error }: onMaxAttemptsFailedParameters) => { + clearTimeout(timeout); + t.truthy(error); + ws.destroy(); + resolve("done"); + }); + + ws.connect(); + }); +}); + +// A server that accepts the upgrade and immediately closes the socket never +// makes the client emit an `error` event – only a `close` one. +class ClosingWebSocket { + binaryType = "arraybuffer"; + identifier = 0; + listeners: Record void)[]> = {}; + + constructor(_url: string) { + setTimeout(() => { + for (const handler of this.listeners.close ?? []) { + handler({ code: 1006, reason: "" }); + } + }, 0); + } + + addEventListener(name: string, handler: (payload: any) => void) { + this.listeners[name] = [...(this.listeners[name] ?? []), handler]; + } + + removeEventListener(name: string, handler: (payload: any) => void) { + this.listeners[name] = (this.listeners[name] ?? []).filter( + (listener) => listener !== handler, + ); + } + + close() {} + + send() {} +} + +test("emits maxAttemptsFailed when the socket closes without an error", async (t) => { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("maxAttemptsFailed was not emitted")); + }, 2000); + + const ws = new HocuspocusProviderWebsocket({ + ...exhaustedRetryOptions, + WebSocketPolyfill: ClosingWebSocket, + onMaxAttemptsFailed({ error }) { + clearTimeout(timeout); + t.truthy(error); + ws.destroy(); + resolve("done"); + }, + }); + t.teardown(() => ws.destroy()); + }); +}); + +test("does not emit maxAttemptsFailed when the connection attempt is aborted", async (t) => { + let failed = false; + + const ws = new HocuspocusProviderWebsocket({ + url: "ws://127.0.0.1:1", + maxAttempts: 5, + delay: 50, + minDelay: 50, + initialDelay: 0, + jitter: false, + onMaxAttemptsFailed() { + failed = true; + }, + }); + t.teardown(() => ws.destroy()); + + // Give the first attempt a chance to fail, then stop retrying. + await sleep(60); + ws.disconnect(); + await sleep(400); + + t.false(failed); +}); diff --git a/tests/providerwebsocket/messageQueueDeduplication.ts b/tests/providerwebsocket/messageQueueDeduplication.ts new file mode 100644 index 0000000..162f7bf --- /dev/null +++ b/tests/providerwebsocket/messageQueueDeduplication.ts @@ -0,0 +1,160 @@ +import test from 'ava' +import * as encoding from 'lib0/encoding' +import { HocuspocusProviderWebsocket } from '@hocuspocus/provider' +import { newHocuspocus } from '../utils/index.ts' + +/** + * Helper to create a raw message Uint8Array with a given documentName and messageType, + * matching the wire format: VarString(documentName) + VarUint(messageType) + optional payload. + */ +function createRawMessage(documentName: string, messageType: number, payload?: Uint8Array): Uint8Array { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, messageType) + if (payload) { + encoding.writeVarUint8Array(encoder, payload) + } + return encoding.toUint8Array(encoder) +} + +// MessageType enum values (matching provider/src/types.ts) +const MessageType = { + Sync: 0, + Awareness: 1, + Auth: 2, + QueryAwareness: 3, + Stateless: 5, + CLOSE: 7, +} + +test('deduplicates awareness messages for the same document in the queue', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + // Queue multiple awareness messages for the same document + const msg1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([1])) + const msg2 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([2])) + const msg3 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([3])) + + ws.send(msg1) + ws.send(msg2) + ws.send(msg3) + + // Access the private messageQueue via cast + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 1, 'should only have the latest awareness message') + t.deepEqual(queue[0], msg3, 'should keep the last queued awareness message') +}) + +test('deduplicates QueryAwareness messages for the same document in the queue', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + const msg1 = createRawMessage('doc1', MessageType.QueryAwareness) + const msg2 = createRawMessage('doc1', MessageType.QueryAwareness) + + ws.send(msg1) + ws.send(msg2) + + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 1, 'should only have the latest QueryAwareness message') + t.deepEqual(queue[0], msg2) +}) + +test('does not deduplicate awareness messages for different documents', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + const msg1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([1])) + const msg2 = createRawMessage('doc2', MessageType.Awareness, new Uint8Array([2])) + + ws.send(msg1) + ws.send(msg2) + + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 2, 'should keep awareness messages for different documents') +}) + +test('does not deduplicate Sync messages', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + const msg1 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([1])) + const msg2 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([2])) + const msg3 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([3])) + + ws.send(msg1) + ws.send(msg2) + ws.send(msg3) + + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 3, 'should keep all Sync messages') +}) + +test('does not deduplicate Stateless messages', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + const msg1 = createRawMessage('doc1', MessageType.Stateless, new Uint8Array([1])) + const msg2 = createRawMessage('doc1', MessageType.Stateless, new Uint8Array([2])) + + ws.send(msg1) + ws.send(msg2) + + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 2, 'should keep all Stateless messages') +}) + +test('deduplicates awareness but preserves sync messages in mixed queue', async t => { + const server = await newHocuspocus(t) + + const ws = new HocuspocusProviderWebsocket({ + url: server.server!.webSocketURL, + autoConnect: false, + }) + t.teardown(() => ws.destroy()) + + const sync1 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([1])) + const awareness1 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([10])) + const sync2 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([2])) + const awareness2 = createRawMessage('doc1', MessageType.Awareness, new Uint8Array([20])) + const sync3 = createRawMessage('doc1', MessageType.Sync, new Uint8Array([3])) + + ws.send(sync1) + ws.send(awareness1) + ws.send(sync2) + ws.send(awareness2) + ws.send(sync3) + + const queue = (ws as any).messageQueue as Uint8Array[] + t.is(queue.length, 4, 'should have 3 sync + 1 awareness') + t.deepEqual(queue[0], sync1) + t.deepEqual(queue[1], sync2) + t.deepEqual(queue[2], awareness2, 'should keep the latest awareness message') + t.deepEqual(queue[3], sync3) +}) diff --git a/tests/server/address.ts b/tests/server/address.ts new file mode 100644 index 0000000..75cf4ad --- /dev/null +++ b/tests/server/address.ts @@ -0,0 +1,61 @@ +import test from 'ava' +import { Server } from '@hocuspocus/server' +import { newHocuspocus } from '../utils/index.ts' + +test('returns a dynamic HTTP/WebSocket address with the correct port', async t => { + const hocuspocus = await newHocuspocus(t, { + port: 4010, + }) + + t.is(hocuspocus.server!.address.port, 4010) + t.is(hocuspocus.server!.httpURL, 'http://0.0.0.0:4010') + t.is(hocuspocus.server!.webSocketURL, 'ws://0.0.0.0:4010') + + t.pass() +}) + +test('binds to the configured address', async t => { + const server = new Server({ + port: 0, + address: '127.0.0.1', + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + await server.listen() + + const addressInfo = server.httpServer.address() + t.truthy(addressInfo && typeof addressInfo === 'object') + if (addressInfo && typeof addressInfo === 'object') { + t.is(addressInfo.address, '127.0.0.1') + t.is(addressInfo.family, 'IPv4') + } + + t.is(server.httpURL, `http://127.0.0.1:${server.address.port}`) + + const response = await fetch(server.httpURL) + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('binds to all interfaces when no address is configured', async t => { + const server = new Server({ + port: 0, + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + await server.listen() + + const addressInfo = server.httpServer.address() + t.truthy(addressInfo && typeof addressInfo === 'object') + if (addressInfo && typeof addressInfo === 'object') { + t.true(['::', '0.0.0.0'].includes(addressInfo.address)) + } + + t.is(server.httpURL, `http://0.0.0.0:${server.address.port}`) + + const response = await fetch(server.httpURL) + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) diff --git a/tests/server/afterHandleMessage.ts b/tests/server/afterHandleMessage.ts new file mode 100644 index 0000000..d559899 --- /dev/null +++ b/tests/server/afterHandleMessage.ts @@ -0,0 +1,102 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('afterHandleMessage is called after the update has been applied', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async afterHandleMessage({ document }) { + // contrary to beforeHandleMessage, the update is already applied here + if (document.getArray('foo').get(0) === 'bar') { + t.pass() + resolve('done') + } + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('afterHandleMessage is called once per handled message', async t => { + let beforeHandleMessageCount = 0 + let afterHandleMessageCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async beforeHandleMessage() { + beforeHandleMessageCount += 1 + }, + async afterHandleMessage() { + afterHandleMessageCount += 1 + }, + }) + + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + }) + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + }) + + resolve('done') + }) + + await retryableAssertion(t, tt => { + tt.true(afterHandleMessageCount > 0) + tt.is(afterHandleMessageCount, beforeHandleMessageCount) + }) +}) + +test('afterHandleMessage is not called when beforeHandleMessage rejects', async t => { + let afterHandleMessageCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async beforeHandleMessage() { + throw new Error() + }, + async afterHandleMessage() { + afterHandleMessageCount += 1 + }, + }) + + newHocuspocusProvider(t, server, { + onClose() { + resolve('done') + }, + }) + }) + + await retryableAssertion(t, tt => { + tt.is(afterHandleMessageCount, 0) + }) +}) + +test('an exception thrown in afterHandleMessage does not close the connection', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async afterHandleMessage() { + throw new Error() + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + onSynced() { + t.pass() + setTimeout(() => resolve('done'), 100) + }, + }) + }) +}) diff --git a/tests/server/afterLoadDocument.ts b/tests/server/afterLoadDocument.ts new file mode 100644 index 0000000..83b1cec --- /dev/null +++ b/tests/server/afterLoadDocument.ts @@ -0,0 +1,65 @@ +import test from 'ava' +import type { HocuspocusProvider } from '@hocuspocus/provider' + +import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts' + +test('executes the afterLoadDocument callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async afterLoadDocument() { + t.pass() + resolve('done') + }, + }) + + newHocuspocusProvider(t, server, {}) + }) +}) + +test('executes the afterLoadDocument callback in an extension', async t => { + await new Promise(async resolve => { + let provider: HocuspocusProvider + + class CustomExtension { + async afterLoadDocument() { + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('does not execute the afterLoadDocument callback when document fails to load', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + class CustomExtension { + async onLoadDocument() { + throw new Error('oops!') + } + + async afterLoadDocument() { + t.fail('this should not be executed') + resolve('done') + } + } + + server.configure({ + extensions: [ + new CustomExtension(), + ], + }) + + newHocuspocusProvider(t, server) + + await sleep(300) + t.pass() + resolve('') + }) +}) diff --git a/tests/server/afterStoreDocument.ts b/tests/server/afterStoreDocument.ts new file mode 100644 index 0000000..61d61c3 --- /dev/null +++ b/tests/server/afterStoreDocument.ts @@ -0,0 +1,47 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('calls the afterStoreDocument hook', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async afterStoreDocument() { + t.pass() + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray('foo').push(['foo']) + provider.configuration.websocketProvider.destroy() + provider.destroy() + }, + }) + }) +}) + +test('executes afterStoreDocument callback from a custom extension', async t => { + await new Promise(async resolve => { + class CustomExtension { + async afterStoreDocument() { + t.pass() + + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) diff --git a/tests/server/afterUnloadDocument.ts b/tests/server/afterUnloadDocument.ts new file mode 100644 index 0000000..f90fa2e --- /dev/null +++ b/tests/server/afterUnloadDocument.ts @@ -0,0 +1,77 @@ +import test from 'ava' +import type { HocuspocusProvider } from '@hocuspocus/provider' + +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the afterUnloadDocument callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async afterUnloadDocument() { + t.pass() + resolve('done') + }, + }) + + const p = newHocuspocusProvider(t, server, { + onSynced(data) { + p.configuration.websocketProvider.disconnect() + p.disconnect() + }, + }) + }) +}) + +test('executes the afterUnloadDocument callback when all clients disconnect after a document was loaded', async t => { + await new Promise(async resolve => { + // eslint-disable-next-line prefer-const + let provider: HocuspocusProvider + + class CustomExtension { + async afterLoadDocument() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + } + + async afterUnloadDocument() { + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }) + + provider = newHocuspocusProvider(t, server) + }) +}) + +test('does not execute the afterUnloadDocument callback when document fails to load', async t => { + await new Promise(async (resolve, reject) => { + const server = await newHocuspocus(t) + + class CustomExtension { + async onLoadDocument() { + throw new Error('oops!') + } + + async afterUnloadDocument() { + t.fail() + reject('should not be called') + } + } + + server.configure({ + extensions: [ + new CustomExtension(), + ], + }) + + newHocuspocusProvider(t, server) + + setTimeout(() => { + t.pass() + resolve('done') + }, 500) + }) +}) diff --git a/tests/server/beforeBroadcastStateless.ts b/tests/server/beforeBroadcastStateless.ts new file mode 100644 index 0000000..c7523cd --- /dev/null +++ b/tests/server/beforeBroadcastStateless.ts @@ -0,0 +1,21 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('calls the beforeBroadcastStateless hook', async t => { + await new Promise(async resolve => { + const payloadToSend = 'STATELESS-MESSAGE' + const server = await newHocuspocus(t, { + async beforeBroadcastStateless({ payload }) { + t.is(payload, payloadToSend) + t.pass() + resolve('done') + }, + }) + + newHocuspocusProvider(t, server, { + onSynced() { + server.documents.get('hocuspocus-test')?.broadcastStateless(payloadToSend) + }, + }) + }) +}) diff --git a/tests/server/beforeHandleAwareness.ts b/tests/server/beforeHandleAwareness.ts new file mode 100644 index 0000000..7472d60 --- /dev/null +++ b/tests/server/beforeHandleAwareness.ts @@ -0,0 +1,239 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('beforeHandleAwareness is called before the awareness state is applied', async t => { + await new Promise(async resolve => { + let resolved = false + + const server = await newHocuspocus(t, { + async beforeHandleAwareness({ awareness, states, connection }) { + if (resolved) return + resolved = true + + // The decoded states from the inbound update are exposed as a + // mutable Map keyed by clientId. The document's own awareness has + // not received the update yet. + t.true(states instanceof Map) + t.true(states.size > 0) + t.truthy(connection, 'connection is forwarded per call') + t.is(awareness.getStates().size, 0) + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + }) +}) + +test('mutating states in beforeHandleAwareness rewrites the update before it is applied', async t => { + await new Promise(async resolve => { + let resolved = false + + const server = await newHocuspocus(t, { + async beforeHandleAwareness({ states }) { + // Stamp a server-side value over whatever the client sent. + for (const state of states.values()) { + state.user = 'Verified' + } + }, + async onAwarenessUpdate({ states }) { + const v = states.find(s => s.user === 'Verified' && s.foo === 'bar') + if (resolved || !v) return + resolved = true + + // Mutated value lands in awareness; original ('Spoofed') never appears. + t.is(v.user, 'Verified') + t.is(v.foo, 'bar') + t.falsy(states.find(s => s.user === 'Spoofed')) + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('user', 'Spoofed') + provider.setAwarenessField('foo', 'bar') + }, + }) + }) +}) + +test('chaining: a second extension sees mutations made by the first', async t => { + await new Promise(async resolve => { + let resolved = false + + class FirstExtension { + async beforeHandleAwareness({ states }: { states: Map> }) { + for (const state of states.values()) { + state.user = 'first' + } + } + } + + class SecondExtension { + async beforeHandleAwareness({ states }: { states: Map> }) { + // Sees `first`'s mutation, appends to it. + for (const state of states.values()) { + t.is(state.user, 'first', 'sees first extension mutation') + state.user = `${state.user}-then-second` + } + } + } + + const server = await newHocuspocus(t, { + extensions: [new FirstExtension(), new SecondExtension()], + async onAwarenessUpdate({ states }) { + const v = states.find(s => s.user === 'first-then-second') + if (resolved || !v) return + resolved = true + + // Both extension mutations made it through, in order. + t.is(v.user, 'first-then-second') + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('user', 'unchanged-by-client') + }, + }) + }) +}) + +test('chaining: extensions run before the config-level hook, in registration order', async t => { + await new Promise(async resolve => { + let resolved = false + + class ExtensionA { + async beforeHandleAwareness({ states }: { states: Map> }) { + for (const state of states.values()) { + state.trail = `${state.trail ?? ''}A` + } + } + } + + class ExtensionB { + async beforeHandleAwareness({ states }: { states: Map> }) { + for (const state of states.values()) { + state.trail = `${state.trail ?? ''}B` + } + } + } + + const server = await newHocuspocus(t, { + extensions: [new ExtensionA(), new ExtensionB()], + async beforeHandleAwareness({ states }) { + // Config-level hook is pushed onto `extensions` as the final entry by + // `configure()`, so it runs AFTER both class extensions. + for (const state of states.values()) { + state.trail = `${state.trail ?? ''}C` + } + }, + async onAwarenessUpdate({ states }) { + const v = states.find(s => s.trail === 'ABC') + if (resolved || !v) return + resolved = true + + t.is(v.trail, 'ABC', 'extensions run first then the config-level hook') + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('init', true) + }, + }) + }) +}) + +test('throwing discards preceding extension mutations', async t => { + await new Promise(async resolve => { + let mutatorCalls = 0 + let throwerCalls = 0 + let onUpdateCalls = 0 + + class MutatingExtension { + async beforeHandleAwareness({ states }: { states: Map> }) { + mutatorCalls += 1 + for (const state of states.values()) { + state.user = 'first' + } + } + } + + class ThrowingExtension { + async beforeHandleAwareness() { + throwerCalls += 1 + throw new Error('rejected') + } + } + + const server = await newHocuspocus(t, { + extensions: [new MutatingExtension(), new ThrowingExtension()], + async onAwarenessUpdate({ states }) { + if (states.length > 0) onUpdateCalls += 1 + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + + setTimeout(() => { + t.true(mutatorCalls >= 1, 'preceding extension fired') + t.true(throwerCalls >= 1, 'throwing extension fired') + t.is(onUpdateCalls, 0, 'preceding mutations are not half-applied') + resolve('done') + }, 400) + }) +}) + +test('throwing aborts subsequent extensions and the config-level hook', async t => { + await new Promise(async resolve => { + let throwerCalls = 0 + let afterThrowerCalls = 0 + let configCalls = 0 + + class ThrowingExtension { + async beforeHandleAwareness() { + throwerCalls += 1 + throw new Error('rejected') + } + } + + class AfterThrowerExtension { + async beforeHandleAwareness() { + afterThrowerCalls += 1 + } + } + + const server = await newHocuspocus(t, { + extensions: [new ThrowingExtension(), new AfterThrowerExtension()], + async beforeHandleAwareness() { + configCalls += 1 + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + + setTimeout(() => { + t.true(throwerCalls >= 1, 'throwing extension fired') + t.is(afterThrowerCalls, 0, 'extensions after the throw do not run') + t.is(configCalls, 0, 'config-level hook does not run') + resolve('done') + }, 400) + }) +}) diff --git a/tests/server/beforeHandleMessage.ts b/tests/server/beforeHandleMessage.ts new file mode 100644 index 0000000..48c9764 --- /dev/null +++ b/tests/server/beforeHandleMessage.ts @@ -0,0 +1,111 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('beforeHandleMessage gets called in proper order', async t => { + await new Promise(async resolve => { + const mockContext = { + user: 123, + } + + const expectedValuesByCallNumber = [ + undefined, // syncstep1 + undefined, // syncstep2 + 'foo', // sync finished, value should be there now + ] + let callNumber = 0 + + const server = await newHocuspocus(t, { + async onConnect() { + return mockContext + }, + async beforeHandleMessage({ document, context }) { + t.deepEqual(context, mockContext) + + const value = document.getArray('foo').get(0) + + t.is(value, expectedValuesByCallNumber[callNumber]) + callNumber += 1 + + if (callNumber === expectedValuesByCallNumber.length - 1) { + resolve('done') + } + }, + async onChange({ context, document }) { + t.deepEqual(context, mockContext) + + const value = document.getArray('foo').get(0) + + t.is(value, expectedValuesByCallNumber[2]) + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('beforeHandleMessage callback is called for every new client', async t => { + let onConnectCount = 0 + let beforeHandleMessageCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + onConnectCount += 1 + }, + async beforeHandleMessage() { + beforeHandleMessageCount += 1 + }, + }) + + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + }) + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + }) + + resolve('done') + }) + + await retryableAssertion(t, tt => { + tt.is(onConnectCount, 2) + tt.is(beforeHandleMessageCount, 6) // 2x awareness per conn, 2x sync per conn (step 1 + 2) + }) + +}) + +test('an exception thrown in beforeHandleMessage closes the connection and discards queued messages', async t => { + let beforeHandleMessageCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async beforeHandleMessage({ connection, update }) { + beforeHandleMessageCount += 1 + + if (beforeHandleMessageCount === 1) { + connection.handleMessage(update) + } + + throw new Error() + }, + }) + + newHocuspocusProvider(t, server, { + onClose() { + t.pass() + resolve('done') + }, + }) + }) + + t.is(beforeHandleMessageCount, 1) +}) diff --git a/tests/server/beforeSync.ts b/tests/server/beforeSync.ts new file mode 100644 index 0000000..7a05314 --- /dev/null +++ b/tests/server/beforeSync.ts @@ -0,0 +1,162 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('beforeSync gets called in proper order', async t => { + await new Promise(async resolve => { + let resolved = false + const mockContext = { + user: 123, + } + + let callNumber = 0 + + const server = await newHocuspocus(t, { + async onConnect() { + return mockContext + }, + async beforeSync({ document, context, payload }) { + if (resolved) return + + t.deepEqual(context, mockContext) + + callNumber += 1 + + if (callNumber === 2) { + resolved = true + resolve('done') + } + }, + async onChange({ context, document }) { + if (resolved) return + + t.deepEqual(context, mockContext) + + const value = document.getArray('foo').get(0) + + t.is(value, 'bar') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('beforeSync callback is called for every sync', async t => { + let onConnectCount = 0 + let updateCount = 0 + let syncstep1Count = 0 + let syncstep2Count = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + onConnectCount += 1 + }, + async beforeSync({ type }) { + if (type === 0){ + syncstep1Count += 1 + } else if (type === 1) { + syncstep2Count += 1 + } else if (type === 2) { + updateCount += 1 + } + }, + }) + + await Promise.all([ + new Promise(done => { + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + onSynced() { + done('done') + }, + }) + }), + new Promise(done => { + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + onSynced() { + done('done') + }, + }) + }), + ]) + + resolve('done') + }) + + await retryableAssertion(t, tt => { + tt.is(onConnectCount, 2) + tt.is(syncstep1Count, 2) + tt.is(syncstep2Count, 2) + tt.is(updateCount, 0) + }) +}) + + +test('beforeSync callback is called on every update', async t => { + let onConnectCount = 0 + let updateCount = 0 + let syncstep1Count = 0 + let syncstep2Count = 0 + + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + onConnectCount += 1 + }, + async beforeSync({ type }) { + if (type === 0){ + syncstep1Count += 1 + } else if (type === 1) { + syncstep2Count += 1 + } else if (type === 2) { + updateCount += 1 + } + }, + }) + + await Promise.all([ + new Promise(done => { + newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + onSynced() { + done('done') + }, + }) + }), + new Promise(done => { + const provider = newHocuspocusProvider(t, server, { + onClose() { + t.fail() + }, + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + done('done') + }, + }) + }), + ]) + + resolve('done') + }) + + await retryableAssertion(t, tt => { + tt.is(onConnectCount, 2) + tt.is(syncstep1Count, 2) + tt.is(syncstep2Count, 2) + tt.is(updateCount, 1) + }) +}) diff --git a/tests/server/beforeUnloadDocument.ts b/tests/server/beforeUnloadDocument.ts new file mode 100644 index 0000000..d87afd4 --- /dev/null +++ b/tests/server/beforeUnloadDocument.ts @@ -0,0 +1,81 @@ +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import test from "ava"; + +import { newHocuspocus, newHocuspocusProvider } from "../utils/index.ts"; + +test("executes the beforeUnloadDocument callback", async (t) => { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async beforeUnloadDocument() { + t.pass(); + resolve("done"); + }, + }); + + const p = newHocuspocusProvider(t, server, { + onSynced(data) { + p.destroy(); + }, + }); + }); +}); + +test("executes the beforeUnloadDocument callback when all clients disconnect after a document was loaded", async (t) => { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + await new Promise(async (resolve) => { + // eslint-disable-next-line prefer-const + let provider: HocuspocusProvider; + + class CustomExtension { + async afterLoadDocument() { + provider.destroy(); + } + + async beforeUnloadDocument() { + t.pass(); + resolve("done"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + provider = newHocuspocusProvider(t, server); + }); +}); + +test("throwing an exception in beforeUnloadDocument prevents a document from being unloaded", async (t) => { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + await new Promise(async (resolve) => { + // eslint-disable-next-line prefer-const + let provider: HocuspocusProvider; + + class CustomExtension { + async beforeUnloadDocument() { + throw new Error("my custom error"); + } + + async afterUnloadDocument() { + t.fail("should not be called"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + const p = newHocuspocusProvider(t, server, { + onSynced(data) { + p.destroy(); + }, + }); + + setTimeout(() => { + t.is(server.documents.size, 1); + t.pass(); + resolve("done"); + }, 500); + }); +}); diff --git a/tests/server/broadcastEncoding.ts b/tests/server/broadcastEncoding.ts new file mode 100644 index 0000000..2766a06 --- /dev/null +++ b/tests/server/broadcastEncoding.ts @@ -0,0 +1,153 @@ +import test from 'ava' +import { makeRoutingKey } from '@hocuspocus/common' +import { Document } from '@hocuspocus/server' +import * as decoding from 'lib0/decoding' + +type FakeConnection = { + messageAddress: string + sent: Uint8Array[] + send: (message: Uint8Array) => void +} + +/** + * Encoding is identical batched or not, so these tests opt out of batching to + * keep every assertion synchronous. + */ +const newDocument = () => new Document('hocuspocus-test', undefined, { flushDelay: false }) + +/** + * Registers a minimal stand-in for a Connection. The broadcast paths only read + * `messageAddress` and call `send`, so a duck-typed object is enough to observe + * what they produce. + */ +const addConnection = (document: Document, messageAddress: string): FakeConnection => { + const connection: FakeConnection = { + messageAddress, + sent: [], + send(message: Uint8Array) { + this.sent.push(message) + }, + } + + document.connections.set(connection as any, { clients: new Set() }) + + return connection +} + +const readAddress = (message: Uint8Array) => decoding.readVarString(decoding.createDecoder(message)) + +test('encodes a document update once and shares it across connections', t => { + const document = newDocument() + + const first = addConnection(document, 'hocuspocus-test') + const second = addConnection(document, 'hocuspocus-test') + const third = addConnection(document, 'hocuspocus-test') + + document.getMap('test').set('foo', 'bar') + + t.is(first.sent.length, 1) + t.is(second.sent.length, 1) + t.is(third.sent.length, 1) + + // The same buffer instance reaches every connection, so it was encoded once. + t.is(first.sent[0], second.sent[0]) + t.is(first.sent[0], third.sent[0]) +}) + +test('encodes an awareness update once and shares it across connections', t => { + const document = newDocument() + + const first = addConnection(document, 'hocuspocus-test') + const second = addConnection(document, 'hocuspocus-test') + + document.awareness.setLocalState({ foo: 'bar' }) + + t.is(first.sent.length, 1) + t.is(second.sent.length, 1) + t.is(first.sent[0], second.sent[0]) +}) + +test('encodes a stateless broadcast once and shares it across connections', t => { + const document = newDocument() + + const first = addConnection(document, 'hocuspocus-test') + const second = addConnection(document, 'hocuspocus-test') + + document.broadcastStateless('{"event":"document.saved"}') + + t.is(first.sent.length, 1) + t.is(second.sent.length, 1) + t.is(first.sent[0], second.sent[0]) +}) + +test('a filtered stateless broadcast only reaches matching connections', t => { + const document = newDocument() + + const first = addConnection(document, 'hocuspocus-test') + const skipped = addConnection(document, 'hocuspocus-test') + const third = addConnection(document, 'hocuspocus-test') + + document.broadcastStateless('{"event":"document.saved"}', connection => connection !== (skipped as any)) + + t.is(first.sent.length, 1) + t.is(skipped.sent.length, 0) + t.is(third.sent.length, 1) + + // A skipped connection in the middle must not cost the others their shared buffer. + t.is(first.sent[0], third.sent[0]) +}) + +test('addresses stateless broadcasts per connection', t => { + const document = newDocument() + + const legacy = addConnection(document, 'hocuspocus-test') + const session = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')) + + document.broadcastStateless('{"event":"document.saved"}') + + t.is(readAddress(legacy.sent[0]), 'hocuspocus-test') + t.is(readAddress(session.sent[0]), makeRoutingKey('hocuspocus-test', 'session-a')) + t.not(legacy.sent[0], session.sent[0]) +}) + +test('reuses the buffer across consecutive connections sharing an address', t => { + const document = newDocument() + + const legacy = addConnection(document, 'hocuspocus-test') + const sessionA = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')) + const sessionAToo = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')) + const sessionB = addConnection(document, makeRoutingKey('hocuspocus-test', 'session-b')) + + document.getMap('test').set('foo', 'bar') + + t.is(sessionA.sent[0], sessionAToo.sent[0]) + t.not(legacy.sent[0], sessionA.sent[0]) + t.not(sessionA.sent[0], sessionB.sent[0]) + + const buffers = new Set([legacy.sent[0], sessionA.sent[0], sessionAToo.sent[0], sessionB.sent[0]]) + t.is(buffers.size, 3) +}) + +test('addresses every broadcast message to the receiving connection', t => { + const document = newDocument() + + // Interleaved addresses: buffer reuse does not apply here, so this also + // covers the fallback path where every connection gets its own encode. + const connections = [ + addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')), + addConnection(document, 'hocuspocus-test'), + addConnection(document, makeRoutingKey('hocuspocus-test', 'session-a')), + addConnection(document, makeRoutingKey('hocuspocus-test', 'session-b')), + ] + + document.getMap('test').set('foo', 'bar') + document.awareness.setLocalState({ foo: 'bar' }) + + for (const connection of connections) { + t.is(connection.sent.length, 2) + + for (const message of connection.sent) { + t.is(readAddress(message), connection.messageAddress) + } + } +}) diff --git a/tests/server/broadcastStatelessBypass.ts b/tests/server/broadcastStatelessBypass.ts new file mode 100644 index 0000000..154292b --- /dev/null +++ b/tests/server/broadcastStatelessBypass.ts @@ -0,0 +1,104 @@ +import test from 'ava' +import * as encoding from 'lib0/encoding' +import { writeAuthentication } from '@hocuspocus/common' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +// Wire-level MessageType values (mirrors packages/server/src/types.ts). +const MessageType = { + Auth: 2, + Stateless: 5, + BroadcastStateless: 6, +} + +const buildAuthFrame = (documentName: string, token: string): Uint8Array => { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, MessageType.Auth) + writeAuthentication(encoder, token) + // providerVersion is optional on the server side, omit it. + return encoding.toUint8Array(encoder) +} + +const buildBroadcastStatelessFrame = (documentName: string, payload: string): Uint8Array => { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, MessageType.BroadcastStateless) + encoding.writeVarString(encoder, payload) + return encoding.toUint8Array(encoder) +} + +const openRawSocket = (url: string): Promise => new Promise((resolve, reject) => { + const ws = new WebSocket(url) + ws.binaryType = 'arraybuffer' + ws.addEventListener('open', () => resolve(ws), { once: true }) + ws.addEventListener('error', (event) => reject(event), { once: true }) +}) + +test('rejects client-sent BroadcastStateless (opcode 6) — payload must not reach peers', async t => { + const documentName = 'hocuspocus-test' + const attackerPayload = '{"spoof":"server-system-banner","marker":"LITMUS"}' + + let onStatelessCalls = 0 + let beforeBroadcastStatelessCalls = 0 + + const server = await newHocuspocus(t, { + async onAuthenticate({ token, connectionConfig }) { + if (token === 'readonly') { + connectionConfig.readOnly = true + } + }, + async onStateless() { + onStatelessCalls += 1 + }, + async beforeBroadcastStateless() { + beforeBroadcastStatelessCalls += 1 + }, + }) + + const received: string[] = [] + const victimReady = new Promise((resolve) => { + newHocuspocusProvider(t, server, { + name: documentName, + token: 'read+write', + onSynced: () => resolve(), + onStateless: ({ payload }) => { + received.push(payload) + }, + }) + }) + + await victimReady + + // Attacker: raw WebSocket using a readOnly token. Sends a single + // BroadcastStateless (opcode 6) frame after a successful auth. The + // server must reject this server-internal opcode and not fan it out. + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + const authenticated = new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('auth timeout')), 5000) + attacker.addEventListener('message', () => { + clearTimeout(timer) + resolve() + }, { once: true }) + }) + + attacker.send(buildAuthFrame(documentName, 'readonly')) + await authenticated + + attacker.send(buildBroadcastStatelessFrame(documentName, attackerPayload)) + + // Give the server time to (incorrectly) fan out, if the bug is present. + await new Promise((resolve) => setTimeout(resolve, 200)) + + t.false( + received.includes(attackerPayload), + 'victim must not receive payloads from client-sent BroadcastStateless frames', + ) + t.is(onStatelessCalls, 0, 'onStateless must not fire for opcode 6 (it is not the Stateless opcode)') + t.is( + beforeBroadcastStatelessCalls, + 0, + 'beforeBroadcastStateless must not fire because the frame is rejected before fan-out', + ) +}) diff --git a/tests/server/closeConnections.ts b/tests/server/closeConnections.ts new file mode 100644 index 0000000..844c43c --- /dev/null +++ b/tests/server/closeConnections.ts @@ -0,0 +1,85 @@ +import test from 'ava' +import { WebSocketStatus } from '@hocuspocus/provider' +import { + newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket, sleep, +} from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +// test('closes all connections', async t => { +// const server = await newHocuspocus(t) +// const socket = newHocuspocusProviderWebsocket(t, server) +// const socket2 = newHocuspocusProviderWebsocket(t, server) + +// const provider = newHocuspocusProvider(t, server, { +// name: 'hocuspocus-test', +// onClose() { +// // Make sure it doesn’t reconnect. +// socket.disconnect() +// }, +// websocketProvider: socket, +// }) + +// const anotherProvider = newHocuspocusProvider(t, server, { +// name: 'hocuspocus-test-2', +// onClose() { +// // Make sure it doesn’t reconnect. +// socket2.disconnect() +// }, +// websocketProvider: socket2, +// }) + +// await sleep(100) + +// server.closeConnections() + +// t.is(server.documents.size, 1) +// }) + +test('closes a specific connection when a documentName is passed', async t => { + const server = await newHocuspocus(t) + const socket = newHocuspocusProviderWebsocket(t, server) + const socket2 = newHocuspocusProviderWebsocket(t, server) + + const provider = newHocuspocusProvider(t, server, { + name: 'hocuspocus-test', + onClose() { + // Make sure it doesn’t reconnect. + socket.disconnect() + }, + websocketProvider: socket, + }) + + const anotherProvider = newHocuspocusProvider(t, server, { + name: 'hocuspocus-test-2', + websocketProvider: socket2, + }) + + await sleep(100) + + server.closeConnections('hocuspocus-test') + + await retryableAssertion(t, tt => { + tt.is(socket.status, WebSocketStatus.Disconnected) + tt.is(socket2.status, WebSocketStatus.Connected) + }) +}) + +// test('uses a proper close event', async t => { +// await new Promise(async resolve => { +// const server = await newHocuspocus(t) + +// newHocuspocusProvider(t, server, { +// name: 'hocuspocus-test', +// onSynced() { +// server.closeConnections() +// }, +// onClose({ event }) { +// // Make sure it doesn’t reconnect. +// t.is(event.code, 1000) +// t.is(event.reason, 'Reset Connection') + +// resolve('done') +// }, +// }) +// }) +// }) diff --git a/tests/server/debounce.ts b/tests/server/debounce.ts new file mode 100644 index 0000000..a036dd1 --- /dev/null +++ b/tests/server/debounce.ts @@ -0,0 +1,31 @@ +import { useDebounce } from "@hocuspocus/server"; +import test from "ava"; +import { sleep } from "../utils/index.ts"; + +test("maxDebounce does not queue an execution per call while a slow execution is in flight", async (t) => { + const { debounce } = useDebounce(); + + let executions = 0; + const func = async () => { + executions += 1; + // the first execution is slower than maxDebounce, simulating a + // database/S3 latency spike during a store + await sleep(executions === 1 ? 500 : 10); + }; + + // Continuous calls, like a busy collaborative document: each call resets + // the debounce timeout, so only the maxDebounce branch triggers executions + // (at most one per 200ms). + const end = Date.now() + 1_000; + while (Date.now() < end) { + debounce("doc", func, 50, 200); + await sleep(20); + } + await sleep(600); // let in-flight and queued executions finish + + // ~1s of continuous calls with maxDebounce=200 should execute ~5 times. + // If run() suspends on the in-flight execution before deleting the timer + // entry, the stale entry keeps satisfying the maxDebounce age check and + // every call during the slow execution queues another run (~25 executions). + t.true(executions <= 8, `executed ${executions} times, expected ~5`); +}); diff --git a/tests/server/destroy.ts b/tests/server/destroy.ts new file mode 100644 index 0000000..ce89de1 --- /dev/null +++ b/tests/server/destroy.ts @@ -0,0 +1,27 @@ +import test from "ava"; +import { Server } from "@hocuspocus/server"; + +test("destroy only runs once when called multiple times", async (t) => { + let destroyed = 0; + + const server = new Server({ + port: 0, + quiet: true, + stopOnSignals: false, + async onDestroy() { + destroyed += 1; + }, + }); + + t.teardown(() => server.httpServer.close()); + + await server.listen(); + + // Concurrent and subsequent calls (e.g. from repeated SIGINT signals) + // must not re-run the shutdown, which would close already-closed + // resources in extensions. + await Promise.all([server.destroy(), server.destroy()]); + await server.destroy(); + + t.is(destroyed, 1); +}); diff --git a/tests/server/flushDelay.ts b/tests/server/flushDelay.ts new file mode 100644 index 0000000..d45ffd4 --- /dev/null +++ b/tests/server/flushDelay.ts @@ -0,0 +1,268 @@ +import test from 'ava' +import { Document } from '@hocuspocus/server' +import * as decoding from 'lib0/decoding' +import * as Y from 'yjs' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +type FakeConnection = { + messageAddress: string + sent: Uint8Array[] + send: (message: Uint8Array) => void +} + +const addConnection = (document: Document): FakeConnection => { + const connection: FakeConnection = { + messageAddress: document.name, + sent: [], + send(message: Uint8Array) { + this.sent.push(message) + }, + } + + document.connections.set(connection as any, { clients: new Set() }) + + return connection +} + +/** Resolves after the flush scheduled with `flushDelay: 0` has run. */ +const tick = () => new Promise(resolve => setImmediate(resolve)) + +const readUpdate = (message: Uint8Array) => { + const decoder = decoding.createDecoder(message) + + decoding.readVarString(decoder) // message address + decoding.readVarUint(decoder) // MessageType.Sync + decoding.readVarUint(decoder) // messageYjsUpdate + + return decoding.readVarUint8Array(decoder) +} + +const readAwarenessStates = (message: Uint8Array) => { + const decoder = decoding.createDecoder(message) + + decoding.readVarString(decoder) // message address + decoding.readVarUint(decoder) // MessageType.Awareness + + const payload = decoding.createDecoder(decoding.readVarUint8Array(decoder)) + const states = new Map() + const count = decoding.readVarUint(payload) + + for (let i = 0; i < count; i += 1) { + const clientId = decoding.readVarUint(payload) + + decoding.readVarUint(payload) // clock + states.set(clientId, JSON.parse(decoding.readVarString(payload))) + } + + return states +} + +test('sends one message per change when flushDelay is false', t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: false }) + const connection = addConnection(document) + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + document.getMap('test').set('c', 3) + + t.is(connection.sent.length, 3) +}) + +test('batches by default', async t => { + const document = new Document('hocuspocus-test') + const connection = addConnection(document) + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + + t.is(document.flushDelay, 0) + t.is(connection.sent.length, 0) + + await tick() + + t.is(connection.sent.length, 1) +}) + +test('a server batches by default', async t => { + const server = await newHocuspocus(t) + + t.is(server.configuration.flushDelay, 0) +}) + +test('merges updates from the same tick into a single message', async t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const connection = addConnection(document) + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + document.getMap('test').set('c', 3) + + // Nothing goes out synchronously; the flush is scheduled for the end of the turn. + t.is(connection.sent.length, 0) + + await tick() + + t.is(connection.sent.length, 1) +}) + +test('the merged message carries every buffered change', async t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const connection = addConnection(document) + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + document.getMap('test').set('c', 3) + + await tick() + + const target = new Y.Doc() + + Y.applyUpdate(target, readUpdate(connection.sent[0])) + + const map = target.getMap('test') + + t.is(map.get('a'), 1) + t.is(map.get('b'), 2) + t.is(map.get('c'), 3) +}) + +test('collapses awareness changes to the latest state of each client', async t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const connection = addConnection(document) + + document.awareness.setLocalState({ cursor: 1 }) + document.awareness.setLocalState({ cursor: 2 }) + document.awareness.setLocalState({ cursor: 3 }) + + t.is(connection.sent.length, 0) + + await tick() + + t.is(connection.sent.length, 1) + + const states = readAwarenessStates(connection.sent[0]) + + t.is(states.size, 1) + t.is(states.get(document.clientID).cursor, 3) +}) + +test('flushes early once flushMaxBytes is buffered', t => { + const document = new Document('hocuspocus-test', undefined, { + flushDelay: 0, + flushMaxBytes: 1, + }) + const connection = addConnection(document) + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + + // Every update trips the limit, so each is sent without waiting for the flush. + t.is(connection.sent.length, 2) +}) + +test('calls onUpdate synchronously for every change even when batching', t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const connection = addConnection(document) + + let updates = 0 + + document.callbacks.onUpdate = () => { + updates += 1 + } + + document.getMap('test').set('a', 1) + document.getMap('test').set('b', 2) + document.getMap('test').set('c', 3) + + // The broadcast is deferred, the hook that drives onChange / onStoreDocument + // and the Redis publish is not. + t.is(updates, 3) + t.is(connection.sent.length, 0) +}) + +test('flush is a no-op when nothing is pending', t => { + const document = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const connection = addConnection(document) + + document.flush() + document.flush() + + t.is(connection.sent.length, 0) +}) + +test('destroy flushes what is still buffered instead of dropping it', async t => { + const batched = new Document('hocuspocus-test', undefined, { flushDelay: 0 }) + const batchedConnection = addConnection(batched) + + batched.getMap('test').set('a', 1) + + t.is(batchedConnection.sent.length, 0) + + batched.destroy() + + // Everything is out by the time destroy returns, and nothing is scheduled + // afterwards. + const afterDestroy = batchedConnection.sent.length + + await tick() + + t.is(batchedConnection.sent.length, afterDestroy) + + // The same document without batching emits exactly as many messages: the + // buffered update plus the awareness teardown that destroy triggers. + const unbatched = new Document('hocuspocus-test', undefined, { flushDelay: false }) + const unbatchedConnection = addConnection(unbatched) + + unbatched.getMap('test').set('a', 1) + unbatched.destroy() + + t.is(batchedConnection.sent.length, unbatchedConnection.sent.length) +}) + +test('a batching server still converges on connected clients', async t => { + const server = await newHocuspocus(t, { flushDelay: 0 }) + + const writer = newHocuspocusProvider(t, server, { name: 'flush-converge' }) + const reader = newHocuspocusProvider(t, server, { name: 'flush-converge' }) + + await new Promise(resolve => reader.on('synced', () => resolve('done'))) + + writer.document.getMap('test').set('a', 1) + writer.document.getMap('test').set('b', 2) + writer.document.getMap('test').set('c', 3) + + await retryableAssertion(t, tt => { + const map = reader.document.getMap('test') + + tt.is(map.get('a'), 1) + tt.is(map.get('b'), 2) + tt.is(map.get('c'), 3) + }) +}) + +test('onChange still fires per change when batching is enabled', async t => { + let changes = 0 + + const server = await newHocuspocus(t, { + flushDelay: 0, + async onChange() { + changes += 1 + }, + }) + + const provider = newHocuspocusProvider(t, server, { + name: 'flush-onchange', + awareness: null, + }) + + await new Promise(resolve => provider.on('synced', () => resolve('done'))) + + provider.document.getMap('test').set('a', 1) + provider.document.getMap('test').set('b', 2) + provider.document.getMap('test').set('c', 3) + + await retryableAssertion(t, tt => { + tt.is(changes, 3) + }) +}) diff --git a/tests/server/getConnectionsCount.ts b/tests/server/getConnectionsCount.ts new file mode 100644 index 0000000..03a4f76 --- /dev/null +++ b/tests/server/getConnectionsCount.ts @@ -0,0 +1,130 @@ +import test from 'ava' +import { retryableAssertion } from '../utils/retryableAssertion.ts' +import { newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket } from '../utils/index.ts' + +test('returns 0 connections when there’s no one connected', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + t.is(server.getConnectionsCount(), 0) + + resolve('done') + }) +}) + +test('close connection open when it fails', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + throw new Error() + }, + }) + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.is(server.getConnectionsCount(), 0) + resolve('done') + }, + }) + }) +}) + +test('dont close connection open when it fails but socket is external', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + throw new Error() + }, + }) + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.is(server.getConnectionsCount(), 0) + resolve('done') + }, + }) + }) +}) + +test('outputs the total connections', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + newHocuspocusProvider(t, server, { + onSynced() { + t.is(server.getConnectionsCount(), 1) + + newHocuspocusProvider(t, server, { + onSynced() { + t.is(server.getConnectionsCount(), 2) + + resolve('done') + }, + }) + }, + }) + }) +}) + +test('total connections includes direct connections', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { name: 'hocuspocus-test' }) + + await server.openDirectConnection('hocuspocus-test') + t.is(server.getConnectionsCount(), 1) + + newHocuspocusProvider(t, server, { + onSynced() { + t.is(server.getConnectionsCount(), 2) + + resolve('done') + }, + }) + }) +}) + +test('adds and removes connections properly', async t => { + const server = await newHocuspocus(t) + + const providers = [ + newHocuspocusProvider(t, server), + newHocuspocusProvider(t, server), + newHocuspocusProvider(t, server), + newHocuspocusProvider(t, server), + newHocuspocusProvider(t, server), + ] + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 5) + }) + + providers.forEach(provider => { provider.disconnect(); provider.configuration.websocketProvider.disconnect() }) + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 0) + }) +}) + +test('multiplexed connections counts properly', async t => { + const server = await newHocuspocus(t) + const socket = newHocuspocusProviderWebsocket(t, server) + + const providers = [ + newHocuspocusProvider(t, server, { name: 'mux-1' }, {}, socket), + newHocuspocusProvider(t, server, { name: 'mux-2' }, {}, socket), + newHocuspocusProvider(t, server, { name: 'mux-3' }, {}, socket), + newHocuspocusProvider(t, server), + newHocuspocusProvider(t, server), + + ] + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 3) + }) + + providers.forEach(provider => { provider.disconnect(); provider.configuration.websocketProvider.disconnect() }) + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 0) + }) +}) diff --git a/tests/server/getDocumentsCount.ts b/tests/server/getDocumentsCount.ts new file mode 100644 index 0000000..87ec73e --- /dev/null +++ b/tests/server/getDocumentsCount.ts @@ -0,0 +1,93 @@ +import test from 'ava' +import { + newHocuspocus, newHocuspocusProvider, randomInteger, +} from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('documents count is zero by default', async t => { + const server = await newHocuspocus(t) + + t.is(server.getDocumentsCount(), 0) +}) + +test('documents count is 1 when one provider is connected', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + newHocuspocusProvider(t, server, { + onSynced() { + t.is(server.getDocumentsCount(), 1) + + resolve('done') + }, + }) + }) +}) + +test('the same document name counts as one document', async t => { + const server = await newHocuspocus(t) + + const providers = [ + newHocuspocusProvider(t, server, { name: 'foobar' }), + newHocuspocusProvider(t, server, { name: 'foobar' }), + ] + + await retryableAssertion(t, tt => { + tt.is(server.getDocumentsCount(), 1) + }) + + providers.forEach(provider => { provider.disconnect(); provider.configuration.websocketProvider.disconnect() }) + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 0) + }) +}) + +test('adds and removes different documents properly', async t => { + const server = await newHocuspocus(t) + + const providers = [ + newHocuspocusProvider(t, server, { name: 'foo-1' }), + newHocuspocusProvider(t, server, { name: 'foo-2' }), + newHocuspocusProvider(t, server, { name: 'foo-3' }), + newHocuspocusProvider(t, server, { name: 'foo-4' }), + newHocuspocusProvider(t, server, { name: 'foo-5' }), + ] + + await retryableAssertion(t, tt => { + tt.is(server.getDocumentsCount(), 5) + }) + + providers.forEach(provider => { provider.disconnect(); provider.configuration.websocketProvider.disconnect() }) + + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), 0) + }) +}) + +test('adds and removes random number of documents properly', async t => { + // random number of providers + const server = await newHocuspocus(t) + const numberOfProviders = randomInteger(10, 100) + const providers = [] + for (let index = 0; index < numberOfProviders; index += 1) { + providers.push( + newHocuspocusProvider(t, server, { name: `foobar-${index}` }), + ) + } + await retryableAssertion(t, tt => { + tt.is(server.getDocumentsCount(), numberOfProviders) + }) + + // random number of disconnects + const numberOfDisconnects = randomInteger(1, numberOfProviders) + for (let index = 0; index < numberOfDisconnects; index += 1) { + providers[index].disconnect() + providers[index].configuration.websocketProvider.disconnect() + } + + // check the count + await retryableAssertion(t, tt => { + tt.is(server.getConnectionsCount(), numberOfProviders - numberOfDisconnects) + }) +}) diff --git a/tests/server/listen.ts b/tests/server/listen.ts new file mode 100644 index 0000000..21a3835 --- /dev/null +++ b/tests/server/listen.ts @@ -0,0 +1,105 @@ +import test from 'ava' +import { Server } from '@hocuspocus/server' +import { newHocuspocus } from '../utils/index.ts' + +test('should respond with OK', async t => { + const hocuspocus = await newHocuspocus(t) + + const response = await fetch(hocuspocus.server!.httpURL) + + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('should respond with status 200', async t => { + const hocuspocus = await newHocuspocus(t) + + const response = await fetch(hocuspocus.server!.httpURL) + + t.is(await response.status, 200) +}) + +test('should respond with OK on a custom port', async t => { + const hocuspocus = await newHocuspocus(t, { + port: 4000, + }) + + const response = await fetch(hocuspocus.server!.httpURL) + + t.is(hocuspocus.server!.address.port, 4000) + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('should respond with OK on a custom port passed to listen()', async t => { + const server = new Server({ + port: 0, + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + server.listen(4001) + + const response = await fetch(server.httpURL) + + t.is(server.address.port, 4001) + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('should take a custom port and a callback', async t => { + const server = new Server({ + port: 0, + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + await new Promise(async resolve => { + server.listen(4002, () => { + resolve('done') + }) + }) + + const response = await fetch(server.httpURL) + + t.is(server.address.port, 4002) + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('should execute a callback', async t => { + const server = new Server({ + port: 0, + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + await new Promise(async resolve => { + server.listen(0, async () => { + resolve('done') + }) + }) + + const response = await fetch(server.httpURL) + + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) + +test('should have the custom port as a parameter in the callback', async t => { + const server = new Server({ + port: 0, + stopOnSignals: false, + }) + + t.teardown(() => server.httpServer.close()) + + await new Promise(async resolve => { + server.listen(0, async ({ port }: any) => { + t.is(port, server.address.port) + resolve('done') + }) + }) + + const response = await fetch(server.httpURL) + + t.is(await response.text(), 'Welcome to Hocuspocus!') +}) diff --git a/tests/server/onAuthenticate.ts b/tests/server/onAuthenticate.ts new file mode 100644 index 0000000..54ca1fe --- /dev/null +++ b/tests/server/onAuthenticate.ts @@ -0,0 +1,333 @@ +import { WebSocketStatus } from "@hocuspocus/provider"; +import type { + onAuthenticatePayload, + onLoadDocumentPayload, +} from "@hocuspocus/server"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + newHocuspocusProviderWebsocket, + sleep, +} from "../utils/index.ts"; +import { retryableAssertion } from "../utils/retryableAssertion.ts"; + +test("executes the onAuthenticate callback", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + t.pass(); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + }); + }); +}); + +test("executes the onAuthenticate callback from a custom extension", async (t) => { + await new Promise(async (resolve) => { + class CustomExtension { + async onAuthenticate() { + t.pass(); + resolve("done"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + }); + }); +}); + +test("confirms the `Token` message with an `Authenticated` message", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + // success + return true; + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + onAuthenticated() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("replies with a `PermissionDenied` message when authentication fails", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + // fail + throw Error(); + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("passes context from onAuthenticate to onLoadDocument", async (t) => { + await new Promise(async (resolve) => { + const mockContext = { + user: 123, + }; + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return mockContext; + }, + async onLoadDocument({ context }: onLoadDocumentPayload) { + t.deepEqual(context, mockContext); + + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + }); + }); +}); + +test("ignores the authentication token when having no onAuthenticate hook", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + onOpen() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("has the authentication token", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }: onAuthenticatePayload) { + t.is(token, "SUPER-SECRET-TOKEN"); + + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + }); + }); +}); + +test("does not disconnect provider when the onAuthenticate hook throws an Error", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + throw new Error(); + }, + // MUST NOT BE CALLED + async onLoadDocument() { + t.fail( + "WARNING: When onAuthenticate fails onLoadDocument must not be called.", + ); + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onClose() { + t.fail(); + }, + token: "SUPER-SECRET-TOKEN", + }); + + await retryableAssertion(t, (tt) => { + tt.is( + provider.configuration.websocketProvider.status, + WebSocketStatus.Connected, + ); + tt.is(server.getDocumentsCount(), 0); + tt.is(server.getConnectionsCount(), 0); + }); +}); + +test("connects with the correct token", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }: onAuthenticatePayload) { + if (token !== "SUPER-SECRET-TOKEN") { + throw new Error(); + } + }, + async onLoadDocument() { + t.pass(); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "SUPER-SECRET-TOKEN", + }); + }); +}); + +test("onAuthenticate has access to document name", async (t) => { + const docName = "superSecretDoc"; + const requiredToken = "SUPER-SECRET-TOKEN"; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token, documentName }: onAuthenticatePayload) { + if (documentName !== docName) { + throw new Error(); + } + + if (token !== requiredToken) { + throw new Error(); + } + }, + }); + + newHocuspocusProvider(t, server, { + token: requiredToken, + name: docName, + onAuthenticated() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("onAuthenticate wrong auth only disconnects affected doc (when multiplexing)", async (t) => { + const docName = "superSecretDoc"; + const requiredToken = "SUPER-SECRET-TOKEN"; + + const server = await newHocuspocus(t, { + async onAuthenticate({ token, documentName }: onAuthenticatePayload) { + if (documentName !== docName) { + throw new Error(); + } + + if (token !== requiredToken) { + throw new Error(); + } + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const providerFail = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "wrongToken", + name: "otherDocu", + onAuthenticated() { + t.fail(); + }, + }); + + await sleep(100); + + const providerOK = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: requiredToken, + name: docName, + onAuthenticationFailed() { + t.fail(); + }, + }); + + await retryableAssertion(t, (tt) => { + tt.is(socket.status, WebSocketStatus.Connected); + tt.is(server.getDocumentsCount(), 1); + tt.is(server.getConnectionsCount(), 1); + }); +}); + +test("onAuthenticate readonly auth only affects 1 doc (when multiplexing)", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ + token, + documentName, + connectionConfig, + }: onAuthenticatePayload) { + if (token === "readonly") { + connectionConfig.readOnly = true; + } + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const providerReadOnly = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "readonly", + name: "doc1", + onAuthenticationFailed() { + t.fail(); + }, + }); + + const providerOK = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "read+write", + name: "doc2", + onAuthenticationFailed() { + t.fail(); + }, + }); + + await retryableAssertion(t, (tt) => { + tt.is(socket.status, WebSocketStatus.Connected); + tt.is(socket.status, WebSocketStatus.Connected); + tt.is(server.getDocumentsCount(), 2); + tt.is(server.getConnectionsCount(), 1); + tt.is(socket.status, WebSocketStatus.Connected); + }); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + t.is( + server.documents.get("doc1")!.connections.keys().next().value! + .readOnly, + true, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + t.is( + server.documents.get("doc2")!.connections.keys().next().value! + .readOnly, + false, + ); +}); + +test("onAuthenticate is called even if no token is provided", async (t) => { + const docName = "superSecretDoc"; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ documentName }: onAuthenticatePayload) { + t.pass(); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + name: docName, + }); + }); +}); diff --git a/tests/server/onAwarenessUpdate.ts b/tests/server/onAwarenessUpdate.ts new file mode 100644 index 0000000..3b48576 --- /dev/null +++ b/tests/server/onAwarenessUpdate.ts @@ -0,0 +1,85 @@ +import test from 'ava' +import type { onAwarenessUpdatePayload } from '@hocuspocus/server' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onAwarenessUpdate callback', async t => { + await new Promise(async resolve => { + let resolved = false + + const server = await newHocuspocus(t, { + async onAwarenessUpdate({ states }) { + if (resolved) return + resolved = true + + t.is(states.length, 1) + t.is(states[0].foo, 'bar') + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + }) +}) + +test('executes the onAwarenessUpdate callback from a custom extension', async t => { + await new Promise(async resolve => { + let resolved = false + + class CustomExtension { + async onAwarenessUpdate({ states }: onAwarenessUpdatePayload) { + if (resolved) return + resolved = true + + t.is(states.length, 1) + t.is(states[0].foo, 'bar') + + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + }) +}) + +test('forwards the originating connection on onAwarenessUpdate', async t => { + await new Promise(async resolve => { + let resolved = false + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return { user: { id: 'u-1', displayName: 'Test User' } } + }, + async onAwarenessUpdate({ connection, states }) { + if (resolved || states.length === 0) return + resolved = true + + t.truthy(connection, 'connection should be defined for client-originated awareness updates') + t.is((connection?.context as { user: { id: string } } | undefined)?.user?.id, 'u-1') + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'anything', + onConnect() { + provider.setAwarenessField('foo', 'bar') + }, + }) + }) +}) diff --git a/tests/server/onChange.ts b/tests/server/onChange.ts new file mode 100644 index 0000000..e378666 --- /dev/null +++ b/tests/server/onChange.ts @@ -0,0 +1,158 @@ +import test from 'ava' +import type { onChangePayload } from '@hocuspocus/server' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('onChange callback receives updates', async t => { + await new Promise(async resolve => { + let resolved = false + const mockContext = { + user: 123, + } + + const server = await newHocuspocus(t, { + async onConnect() { + return mockContext + }, + async onChange({ document, context }) { + if (resolved) return + resolved = true + + t.deepEqual(context, mockContext) + + const value = document.getArray('foo').get(0) + t.is(value, 'bar') + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('executes onChange callback from an extension', async t => { + await new Promise(async resolve => { + let resolved = false + + class CustomExtension { + async onChange({ document }: onChangePayload) { + if (resolved) return + resolved = true + + const value = document.getArray('foo').get(0) + + t.is(value, 'bar') + + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('onChange callback is not called after onLoadDocument', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onChange(data) { + t.fail() + }, + async onLoadDocument({ document }) { + document.getArray('foo').insert(0, ['bar']) + + return document + }, + }) + + newHocuspocusProvider(t, server, { + onSynced() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test('has the server instance', async t => { + await new Promise(async resolve => { + let resolved = false + + const server = await newHocuspocus(t, { + async onChange({ instance }) { + if (resolved) return + resolved = true + + t.is(instance, server) + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getArray('foo').insert(0, ['bar']) + }, + }) + }) +}) + +test('onChange callback isn\'t called for every new client', async t => { + let onConnectCount = 0 + let onChangeCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onConnect() { + onConnectCount += 1 + }, + async onChange() { + onChangeCount += 1 + }, + }) + + newHocuspocusProvider(t, server) + newHocuspocusProvider(t, server) + + resolve('done') + }) + + await retryableAssertion(t, tt => { + tt.is(onConnectCount, 2) + tt.is(onChangeCount, 0) + }) + +}) + +test('onChange works properly for changes from direct connections', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + name: 'hocuspocus-test', + async onChange(data) { + resolve('') + t.pass() + }, + }) + + const conn = await server.openDirectConnection('hocuspocus-test') + t.is(server.getConnectionsCount(), 1) + + conn.transact(doc => { + doc.getMap('t').set('g', 'b') + }) + }) +}) diff --git a/tests/server/onClose.ts b/tests/server/onClose.ts new file mode 100644 index 0000000..2f9fa05 --- /dev/null +++ b/tests/server/onClose.ts @@ -0,0 +1,70 @@ +import test from 'ava' +import { WebSocketStatus } from '@hocuspocus/provider' +import { newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket } from '../utils/index.ts' +import { retryableAssertion } from '../utils/retryableAssertion.ts' + +test('server closes connection when receiving close event from provider', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, {}) + const socket = newHocuspocusProviderWebsocket(t, server, {}) + + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + name: 'hocuspocus-test', + }) + + await retryableAssertion(t, t2 => { + t2.is(server.getConnectionsCount(), 1) + }) + + await retryableAssertion(t, t2 => { + provider1.destroy() + t2.is(server.getConnectionsCount(), 0) + }) + + resolve('ok') + }) +}) + +test('server doesnt close connection after receiving close event from all connections', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, {}) + const socket = newHocuspocusProviderWebsocket(t, server, {}) + + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + name: 'hocuspocus-test', + }) + + const provider2 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + name: 'hocuspocus-test2', + }) + + await retryableAssertion(t, t2 => { + t2.is(server.getConnectionsCount(), 1) + }) + + socket.shouldConnect = false + provider1.destroy() + + t.is(provider1.configuration.websocketProvider.status, WebSocketStatus.Connected) + t.is(provider2.configuration.websocketProvider.status, WebSocketStatus.Connected) + + setTimeout(async () => { + t.is(server.getConnectionsCount(), 1) + provider2.destroy() + + t.is(provider1.configuration.websocketProvider.status, WebSocketStatus.Connected) + t.is(provider2.configuration.websocketProvider.status, WebSocketStatus.Connected) + + await retryableAssertion(t, t2 => { + t2.is(server.getConnectionsCount(), 1) + t2.is(provider1.configuration.websocketProvider.status, WebSocketStatus.Connected) + t2.is(provider2.configuration.websocketProvider.status, WebSocketStatus.Connected) + }) + + resolve('ok') + }, 200) + }) +}) diff --git a/tests/server/onConfigure.ts b/tests/server/onConfigure.ts new file mode 100644 index 0000000..1cc5076 --- /dev/null +++ b/tests/server/onConfigure.ts @@ -0,0 +1,62 @@ +import test from 'ava' +import type { Hocuspocus } from '@hocuspocus/server' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('onConfigure callback is executed', async t => { + await new Promise(async resolve => { + let givenInstance = null + + const server = await newHocuspocus(t, { + async onConfigure({ instance }) { + givenInstance = instance + }, + }) + + t.is(givenInstance as unknown as Hocuspocus, server) + resolve('done') + }) +}) + +test('executes onConfigure callback from an extension', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onConfigure() { + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('has the configuration', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + debounce: 2001, + async onConfigure({ configuration }) { + t.is(configuration.debounce, 2001) + + resolve('done') + }, + }) + }) +}) + +test('has the version', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + async onConfigure({ version }) { + t.truthy(version) + + resolve('done') + }, + }) + }) +}) diff --git a/tests/server/onConnect.ts b/tests/server/onConnect.ts new file mode 100644 index 0000000..8feae95 --- /dev/null +++ b/tests/server/onConnect.ts @@ -0,0 +1,330 @@ +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import test from "ava"; +import { newHocuspocus, newHocuspocusProvider, sleep } from "../utils/index.ts"; + +test("executes the onConnect callback", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect() { + t.pass(); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("refuses connection when an error is thrown", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect() { + throw new Error(); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("executes the onConnect callback from an extension", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + class CustomExtension { + async onConnect() { + t.pass(); + resolve("done"); + } + } + + server.configure({ + extensions: [new CustomExtension()], + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has the document name", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ documentName }) { + t.is(documentName, "hocuspocus-test"); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("sets the provider to readOnly", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ connectionConfig }) { + connectionConfig.readOnly = true; + }, + }); + + newHocuspocusProvider(t, server, { + onSynced() { + for (const connection of server.documents.get("hocuspocus-test")?.connections.keys() ?? []) { + t.is(connection.readOnly, true); + } + + resolve("done"); + }, + }); + }); +}); + +const weirdDocumentNames = [ + "not-weird", + "äöü", + "<>{}|^ß", + "with space", + "with/slash", + "with\backslash", + "a-very-long-document-name-which-should-not-make-any-problems-at-all", + "🌟", + ":", + "—", + "漢", + "triple space", + "*", +]; + +weirdDocumentNames.forEach((weirdDocumentName) => { + test(`encodes weird document names: "${weirdDocumentName}"`, async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ documentName }) { + t.is(documentName, weirdDocumentName); + + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + name: weirdDocumentName, + }); + }); + }); +}); + +test("rejects empty document name via WebSocket", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + newHocuspocusProvider(t, server, { + name: "", + onAuthenticationFailed() { + t.is(server.getDocumentsCount(), 0); + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("rejects whitespace-only document name via WebSocket", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + newHocuspocusProvider(t, server, { + name: " ", + onAuthenticationFailed() { + t.is(server.getDocumentsCount(), 0); + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("stops when the onConnect hook throws an Error", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onConnect() { + throw new Error(); + }, + // MUST NOT BE CALLED + async onLoadDocument() { + t.fail( + "WARNING: When onConnect fails onLoadDocument must not be called.", + ); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("stops when the onConnect hook returns a rejecting promise", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onConnect() { + return Promise.reject(); + }, + // MUST NOT BE CALLED + async onLoadDocument() { + t.fail( + "WARNING: When onConnect fails onLoadDocument must not be called.", + ); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("has the request headers", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ requestHeaders }) { + t.is(requestHeaders.get("connection") !== null, true); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has the whole request", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ request }) { + t.is(new URL(request.url).pathname, "/"); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has the socketId", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ socketId }) { + t.is(socketId !== undefined, true); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has the server instance", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ instance }) { + t.is(instance, server); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("defaults to readOnly = false", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ connectionConfig }) { + t.is(connectionConfig.readOnly, false); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("cleans up correctly when provider disconnects during onLoadDocument", async (t) => { + await new Promise(async (resolve) => { + // eslint-disable-next-line prefer-const + let provider: HocuspocusProvider; + + const server = await newHocuspocus(t, { + onLoadDocument: async () => { + provider.configuration.websocketProvider.disconnect(); + provider.disconnect(); + + // pretent we loaded data from async source + await sleep(100); + }, + }); + + provider = newHocuspocusProvider(t, server, { + name: "super-unique-name", + async onDisconnect() { + await sleep(100); + + t.is( + server.documents.get("super-unique-name"), + undefined, + "no documents", + ); + resolve("done"); + }, + }); + }); +}); + +test("the connections count is correct", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async connected() { + t.is(server.getConnectionsCount(), 1); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has connection.readOnly", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ connectionConfig }) { + t.is(connectionConfig.readOnly, false); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("has the request", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ request }) { + t.truthy(request.url); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); diff --git a/tests/server/onDestroy.ts b/tests/server/onDestroy.ts new file mode 100644 index 0000000..6a0f21a --- /dev/null +++ b/tests/server/onDestroy.ts @@ -0,0 +1,275 @@ +import test from "ava"; +import { newHocuspocus, newHocuspocusProvider } from "../utils/index.ts"; +import { retryableAssertion } from "../utils/retryableAssertion.ts"; + +test("executes the onDestroy hook and has the instance", async (t) => { + await new Promise(async (resolve) => { + const hocuspocus = await newHocuspocus(t, { + async onDestroy({ instance }) { + t.is(instance, hocuspocus); + + resolve("done"); + }, + }); + + await hocuspocus.server!.destroy(); + }); +}); + +test("destroy works if no document is open", async (t) => { + await new Promise(async (resolve) => { + const hocuspocus = await newHocuspocus(t); + + await hocuspocus.server!.destroy(); + + t.pass(); + resolve(""); + }); +}); + +test("executes the onDestroy hook from a custom extension", async (t) => { + await new Promise(async (resolve) => { + class CustomExtension { + async onDestroy() { + t.pass(); + + resolve("done"); + } + } + + const hocuspocus = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + await hocuspocus.server!.destroy(); + }); +}); + +test("destroy closes all connections", async (t) => { + await new Promise(async (resolve) => { + const hocuspocus = await newHocuspocus(t); + + const provider1 = newHocuspocusProvider(t, hocuspocus); + + await retryableAssertion(t, (t2) => t2.is(provider1.synced, true)); + + t.is(hocuspocus.getConnectionsCount(), 1); + t.is(hocuspocus.getDocumentsCount(), 1); + + await hocuspocus.server!.destroy(); + + t.is(hocuspocus.getConnectionsCount(), 0); + t.is(hocuspocus.getDocumentsCount(), 0); + + resolve(""); + }); +}); + +test("destroy does not call onStoreDocument if nothing debounced", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument() { + t.fail(); + }, + }); + + const provider = newHocuspocusProvider(t, server); + + await retryableAssertion(t, (t2) => t2.is(provider.synced, true)); + + await server.server!.destroy(); + + resolve(""); + }); +}); + +test("destroy does not call onStoreDocument after debounced onStoreDocument executes", async (t) => { + await new Promise(async (resolve) => { + let called = 0; + + const server = await newHocuspocus(t, { + debounce: 200, + unloadImmediately: true, + async onStoreDocument() { + called += 1; + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + }, + }); + + await retryableAssertion(t, (t2) => t2.is(provider.synced, true)); + + // Wait for the debounced onStoreDocument to execute + await new Promise((r) => setTimeout(r, 400)); + + await server.server!.destroy(); + + t.is(called, 1); + + resolve(""); + }); +}); + +test("destroy calls onStoreDocument before returning if debounced", async (t) => { + await new Promise(async (resolve) => { + let called = false; + + const hocuspocus = await newHocuspocus(t, { + async onStoreDocument() { + called = true; + }, + }); + + const provider = newHocuspocusProvider(t, hocuspocus, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + }, + }); + + const provider1 = newHocuspocusProvider(t, hocuspocus); + + await retryableAssertion(t, (t2) => t2.is(provider1.synced, true)); + + t.is(called, false); + await hocuspocus.server!.destroy(); + t.is(called, true); + + resolve(""); + }); +}); + +test("destroy calls onStoreDocument before returning, even with unloadImmediately=false if debounced", async (t) => { + await new Promise(async (resolve) => { + let called = false; + + const hocuspocus = await newHocuspocus(t, { + async onStoreDocument() { + called = true; + }, + unloadImmediately: false, + }); + + const provider = newHocuspocusProvider(t, hocuspocus, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + }, + }); + + const provider1 = newHocuspocusProvider(t, hocuspocus); + + await retryableAssertion(t, (t2) => t2.is(provider1.synced, true)); + await retryableAssertion(t, (t2) => t2.is(provider.synced, true)); + + t.is(called, false); + await hocuspocus.server!.destroy(); + t.is(called, true); + + resolve(""); + }); +}); + +test("destroy calls onStoreDocument before returning, even with unloadImmediately=false, with multiple docs if debounced", async (t) => { + await new Promise(async (resolve) => { + let called = 0; + + const hocuspocus = await newHocuspocus(t, { + async onStoreDocument() { + called += 1; + }, + unloadImmediately: false, + }); + + const provider1 = newHocuspocusProvider(t, hocuspocus, { + name: "test1", + onSynced() { + provider1.document.getArray("foo").push(["foo"]); + }, + }); + const provider2 = newHocuspocusProvider(t, hocuspocus, { + name: "test2", + onSynced() { + provider2.document.getArray("foo").push(["foo"]); + }, + }); + const provider3 = newHocuspocusProvider(t, hocuspocus, { + name: "test3", + onSynced() { + provider3.document.getArray("foo").push(["foo"]); + }, + }); + + await retryableAssertion(t, (t2) => t2.is(provider1.synced, true)); + await retryableAssertion(t, (t2) => t2.is(provider2.synced, true)); + await retryableAssertion(t, (t2) => t2.is(provider3.synced, true)); + + // Wait for all changes to reach the server and trigger debounced stores + await retryableAssertion(t, (t2) => { + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test1")); + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test2")); + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test3")); + }); + + t.is(called, 0); + await hocuspocus.server!.destroy(); + await retryableAssertion(t, (t2) => t2.is(called, 3)); + + resolve(""); + }); +}); + +test("destroy calls onStoreDocument before returning, with multiple docs if debounced", async (t) => { + await new Promise(async (resolve) => { + let called = 0; + + const hocuspocus = await newHocuspocus(t, { + async onStoreDocument() { + called += 1; + }, + unloadImmediately: true, + }); + + const provider1 = newHocuspocusProvider(t, hocuspocus, { + name: "test1", + onSynced() { + provider1.document.getArray("foo").push(["foo"]); + }, + }); + const provider2 = newHocuspocusProvider(t, hocuspocus, { + name: "test2", + onSynced() { + provider2.document.getArray("foo").push(["foo"]); + }, + }); + const provider3 = newHocuspocusProvider(t, hocuspocus, { + name: "test3", + onSynced() { + provider3.document.getArray("foo").push(["foo"]); + }, + }); + + await retryableAssertion(t, (t2) => t2.is(provider1.synced, true)); + await retryableAssertion(t, (t2) => t2.is(provider2.synced, true)); + await retryableAssertion(t, (t2) => t2.is(provider3.synced, true)); + + // Wait for all changes to reach the server and trigger debounced stores + await retryableAssertion(t, (t2) => { + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test1")); + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test2")); + t2.true(hocuspocus.debouncer.isDebounced("onStoreDocument-test3")); + }); + + t.is(called, 0); + await hocuspocus.server!.destroy(); + + await retryableAssertion(t, (t2) => t2.is(called, 3)); + + resolve(""); + }); +}); diff --git a/tests/server/onDisconnect.ts b/tests/server/onDisconnect.ts new file mode 100644 index 0000000..1a6d4eb --- /dev/null +++ b/tests/server/onDisconnect.ts @@ -0,0 +1,114 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onDisconnect callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onDisconnect() { + t.pass() + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + }) + }) +}) + +test('executes the onDisconnect callback from an extension', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + class CustomExtension { + async onDisconnect() { + t.pass() + resolve('done') + } + } + + server.configure({ + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + }) + }) +}) + +test('passes the context to the onLoadDocument callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t) + + const mockContext = { + user: 123, + } + + server.configure({ + async onConnect() { + return mockContext + }, + async onDisconnect({ context }) { + t.deepEqual(context, mockContext) + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + }) + }) +}) + +test('has the server instance', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onDisconnect({ instance }) { + t.is(instance, server) + + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + }) + }) +}) + +test('the connections count is correct', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onDisconnect() { + t.is(server.getConnectionsCount(), 0) + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + onConnect() { + provider.configuration.websocketProvider.disconnect() + provider.disconnect() + }, + }) + }) +}) diff --git a/tests/server/onListen.ts b/tests/server/onListen.ts new file mode 100644 index 0000000..8418f87 --- /dev/null +++ b/tests/server/onListen.ts @@ -0,0 +1,52 @@ +import test from 'ava' +import { newHocuspocus } from '../utils/index.ts' + +test('executes the onListen callback', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + async onListen() { + t.pass() + resolve('done') + }, + }) + }) +}) + +test('executes the onListen callback from an extension', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onListen() { + t.pass() + resolve('done') + } + } + + newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + }) +}) + +test('has the configuration', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + async onListen({ configuration }) { + t.is(configuration.quiet, true) + resolve('done') + }, + }) + }) +}) + +test('has the port', async t => { + await new Promise(async resolve => { + newHocuspocus(t, { + async onListen({ port }) { + t.truthy(port) + resolve('done') + }, + }) + }) +}) diff --git a/tests/server/onLoadDocument.ts b/tests/server/onLoadDocument.ts new file mode 100644 index 0000000..482bef4 --- /dev/null +++ b/tests/server/onLoadDocument.ts @@ -0,0 +1,518 @@ +import test from "ava"; +import * as Y from "yjs"; +import { newHocuspocus, newHocuspocusProvider } from "../utils/index.ts"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +test("executes the onLoadDocument callback", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument() { + t.pass(); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("executes the onLoadDocument callback from an extension", async (t) => { + await new Promise(async (resolve) => { + class CustomExtension { + async onLoadDocument() { + t.pass(); + resolve("done"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("passes the context and connection to the onLoadDocument callback", async (t) => { + await new Promise(async (resolve) => { + const mockContext = { + user: 123, + }; + + const server = await newHocuspocus(t, { + async onConnect({ connectionConfig }) { + connectionConfig.readOnly = true; + return mockContext; + }, + async onLoadDocument({ context, connectionConfig }) { + t.deepEqual(context, mockContext); + t.deepEqual(connectionConfig, { + readOnly: true, + isAuthenticated: true, + }); + + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +test("sets the provider to readOnly", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument({ connectionConfig }) { + connectionConfig.readOnly = true; + }, + }); + + newHocuspocusProvider(t, server, { + onSynced() { + for (const connection of server.documents.get("hocuspocus-test")?.connections.keys() ?? []) { + t.is(connection.readOnly, true); + } + resolve("done"); + }, + }); + }); +}); + +test("creates a new document in the onLoadDocument callback", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onLoadDocument({ document }) { + // delay more accurately simulates a database fetch + return new Promise(async (resolve) => { + setTimeout(() => { + document.getArray("foo").insert(0, ["bar"]); + resolve(document); + }, 200); + }); + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + const value = provider.document.getArray("foo").get(0); + t.is(value, "bar"); + + resolve("done"); + }, + }); + }); +}); + +test("does not apply the document to itself when onLoadDocument returns it", async (t) => { + const origins: unknown[] = []; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument({ document }) { + document.on("afterTransaction", (transaction) => { + origins.push(transaction.origin); + }); + + // the content this hook "loaded", written with an explicit origin so + // it doubles as a control: without it, a listener that never fires + // would satisfy the assertion below vacuously + document.transact(() => { + document.getArray("foo").insert(0, ["bar"]); + }, "loaded"); + + return document; + }, + }); + + newHocuspocusProvider(t, server, { + onSynced() { + resolve("done"); + }, + }); + }); + + t.true(origins.includes("loaded"), "the afterTransaction listener never fired"); + + // the self-apply calls applyUpdate() without an origin, and the client sync + // carries a ConnectionTransactionOrigin, so a null origin can only be the + // document having been applied to itself + const selfApplies = origins.filter((origin) => origin == null); + + t.is( + selfApplies.length, + 0, + `expected no null-origin transaction after onLoadDocument returned the same document, got ${selfApplies.length}`, + ); +}); + +test("applies a document returned by the onLoadDocument callback", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument() { + const loaded = new Y.Doc(); + loaded.getArray("foo").insert(0, ["bar"]); + + return loaded; + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + t.is(provider.document.getArray("foo").get(0), "bar"); + + resolve("done"); + }, + }); + }); +}); + +test("multiple simultaneous connections do not create multiple documents", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onLoadDocument({ document }) { + // delay more accurately simulates a database fetch + return new Promise(async (resolve) => { + setTimeout(() => { + document.getArray("foo").insert(0, ["bar"]); + resolve(document); + }, 200); + }); + }, + }); + + const provider = newHocuspocusProvider(t, server); + + newHocuspocusProvider(t, server); + + provider.on("synced", () => { + t.is(server.documents.size, 1); + + resolve("done"); + }); + }); +}); + +test("multiple simultaneous connections wait for the document to be loaded", async (t) => { + t.plan(6); + + await new Promise(async (resolve) => { + let resolveOnLoadDocument: () => void = () => {}; + + const server = await newHocuspocus(t, { + onLoadDocument({ document }) { + // delay more accurately simulates a database fetch + return new Promise(async (innerResolve) => { + resolveOnLoadDocument = () => { + document.getArray("foo").insert(0, ["bar"]); + innerResolve(document); + }; + }); + }, + }); + + const provider1 = newHocuspocusProvider(t, server); + const provider2 = newHocuspocusProvider(t, server); + let provider1Synced = false; + let provider2Synced = false; + + provider1.on("synced", () => { + provider1Synced = true; + const value = provider1.document.getArray("foo").get(0); + t.is(value, "bar"); + }); + provider2.on("synced", () => { + provider2Synced = true; + const value = provider2.document.getArray("foo").get(0); + t.is(value, "bar"); + }); + + await sleep(200); + + t.false(provider1Synced, "provider1Synced"); + t.false(provider2Synced, "provider2Synced"); + + resolveOnLoadDocument(); + + await sleep(200); + + t.true(provider1Synced, "provider1Synced"); + t.true(provider2Synced, "provider2Synced"); + + resolve("done"); + }); +}); + +test("has the server instance", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument({ instance }) { + t.is(instance, server); + + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server); + }); +}); + +/** + * When onLoadDocument fails (for whatever reason), the connection attempt will fail. + */ +test("stops when an error is thrown in onLoadDocument", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument() { + throw new Error(); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + }); + }); +}); + +test("stops when an error is thrown in onLoadDocument, even when authenticated", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + async onLoadDocument() { + throw new Error(); + }, + }); + + newHocuspocusProvider(t, server, { + token: "super-secret-token", + onAuthenticationFailed() { + t.pass(); + resolve("done"); + }, + onClose() { + t.fail(); + }, + }); + }); +}); + +test("disconnects all clients related to the document when an error is thrown in onLoadDocument", async (t) => { + const resolvesNeeded = 2; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument() { + return new Promise((resolve, fail) => { + setTimeout(() => { + // eslint-disable-next-line prefer-promise-reject-errors + fail("ERROR"); + }, 250); + }); + }, + async onStoreDocument(data) { + t.fail("MUST NOT call onStoreDocument"); + }, + }); + + let resolvedNumber = 0; + const resolver = () => { + resolvedNumber += 1; + + if (resolvedNumber >= resolvesNeeded) { + t.is(server.documents.size, 0); + t.is(server.getConnectionsCount(), 0); + resolve("done"); + } + }; + + const provider1 = newHocuspocusProvider(t, server, { + onAuthenticationFailed(event) { + provider1.disconnect(); + resolver(); + }, + }); + + const provider2 = newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + provider2.disconnect(); + resolver(); + }, + }); + }); +}); + +test("destroys the document when onLoadDocument throws", async (t) => { + let loadedDocument: Y.Doc | undefined; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument({ document }) { + loadedDocument = document; + + throw new Error("Something went wrong"); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed() { + resolve("done"); + }, + }); + }); + + t.truthy(loadedDocument, "onLoadDocument never ran"); + + // Awareness starts a setInterval in its constructor that is only cleared by + // the document's destroy(), so a document that is never destroyed leaves a + // live timer behind for every failed load + t.true( + loadedDocument?.isDestroyed, + "the document created for the failed load was never destroyed", + ); +}); + +test("if a new connection connects while the previous connection still fetches the document, it will just work properly", async (t) => { + t.plan(11); + + let callsToOnLoadDocument = 0; + const resolvesNeeded = 10; + + await new Promise(async (resolve) => { + let resolvedNumber = 0; + const resolver = () => { + resolvedNumber += 1; + + if (resolvedNumber >= resolvesNeeded) { + t.is(callsToOnLoadDocument, 1); + resolve("done"); + } + }; + + const server = await newHocuspocus(t, { + // This test asserts on the document state at each individual inbound + // message, so it pins the unbatched fan-out. With the default + // `flushDelay: 0` a broadcast lands one event loop turn later, which + // reorders it relative to the sync acks and shifts these indices. + flushDelay: false, + onLoadDocument({ document }) { + return new Promise(async (resolve) => { + setTimeout(() => { + callsToOnLoadDocument += 1; + document + .getArray("foo") + .insert(0, [`bar-${callsToOnLoadDocument}`]); + resolve(document); + }, 5000); + }); + }, + }); + + let provider1MessagesReceived = 0; + const provider = newHocuspocusProvider(t, server, { + onSynced({ state }) { + // if (!state) return + t.is(server.documents.size, 1); + + const value = provider.document.getArray("foo").get(0); + t.is(value, "bar-1"); + + setTimeout(() => { + provider.document + .getArray("foo") + .insert(0, ["bar-updatedAfterProvider1Synced"]); + }, 100); + + resolver(); + }, + onMessage() { + if (!provider.isSynced) return; + provider1MessagesReceived += 1; + + const value = provider.document.getArray("foo").get(0); + + if (provider1MessagesReceived === 1) { + // do nothing, this is just the ACK for the sync + } else if (provider1MessagesReceived === 2) { + // do nothing, this is just the ACK for the received update (set "bar-updatedAfterProvider1Synced") + } else if (provider1MessagesReceived === 3) { + t.is(value, "bar-updatedAfterProvider1Synced"); + } else { + t.is(value, "bar-updatedAfterProvider2ReceivedMessageFrom1"); + } + + resolver(); + }, + }); + + let provider2MessagesReceived = 0; + setTimeout(() => { + const provider2 = newHocuspocusProvider(t, server, { + onSynced({ state }) { + // if (!state) return + + t.is(server.documents.size, 1); + + const value = provider2.document.getArray("foo").get(0); + t.is(value, "bar-1"); + + resolver(); + }, + onMessage(data) { + if (!provider2.isSynced) return; + provider2MessagesReceived += 1; + + setTimeout(() => { + const value = provider2.document.getArray("foo").get(0); + + if (provider2MessagesReceived === 1) { + // initial state is now synced + t.is(value, "bar-1"); + } else if (provider2MessagesReceived === 2) { + t.is(value, "bar-updatedAfterProvider1Synced"); + setTimeout(() => { + provider.document + .getArray("foo") + .insert(0, ["bar-updatedAfterProvider2ReceivedMessageFrom1"]); + }, 100); + } else { + t.is(value, "bar-updatedAfterProvider2ReceivedMessageFrom1"); + } + resolver(); + }); + }, + }); + }, 2000); + }); +}); + +test("stops when an error is thrown in onLoadDocument and passed on to the provider", async (t) => { + class CustomError extends Error { + code = 1234; + reason = "mycustomreason"; + + constructor(documentName: string) { + super(`Failed loading ${documentName}`); + } + } + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onLoadDocument() { + throw new CustomError("testdoc"); + }, + }); + + newHocuspocusProvider(t, server, { + onAuthenticationFailed(e) { + t.is(e.reason, "mycustomreason"); + t.pass(); + resolve("done"); + }, + }); + }); +}); diff --git a/tests/server/onRequest.ts b/tests/server/onRequest.ts new file mode 100644 index 0000000..1dfd1f6 --- /dev/null +++ b/tests/server/onRequest.ts @@ -0,0 +1,81 @@ +import test from 'ava' +import type { onRequestPayload } from '@hocuspocus/server' +import { newHocuspocus } from '../utils/index.ts' + +test('executes the onRequest callback', async t => { + await new Promise(async resolve => { + const hocuspocus = await newHocuspocus(t, { + async onRequest({ request }: onRequestPayload) { + t.is(request.url, '/foobar') + + resolve('done') + }, + }) + + fetch(`${hocuspocus.server!.httpURL}/foobar`).catch(() => {}) + }) +}) + +test('executes the onRequest callback of a custom extension', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onRequest({ response }: onRequestPayload) { + return new Promise((resolve, reject) => { + + response.writeHead(200, { 'Content-Type': 'text/plain' }) + response.end('I like cats.') + + return reject() + }) + } + } + + const hocuspocus = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const response = await fetch(hocuspocus.server!.httpURL) + t.is(await response.text(), 'I like cats.') + resolve('done') + }) +}) + +test('can intercept specific URLs', async t => { + await new Promise(async resolve => { + const hocuspocus = await newHocuspocus(t, { + async onRequest({ response, request }: onRequestPayload) { + if (request.url === '/foobar') { + return new Promise((resolve, reject) => { + + response.writeHead(200, { 'Content-Type': 'text/plain' }) + response.end('I like cats.') + + return reject() + }) + } + }, + }) + + const interceptedResponse = await fetch(`${hocuspocus.server!.httpURL}/foobar`) + t.is(await interceptedResponse.text(), 'I like cats.') + + const regularResponse = await fetch(hocuspocus.server!.httpURL) + t.is(await regularResponse.text(), 'Welcome to Hocuspocus!') + resolve('done') + }) +}) + +test('has the instance', async t => { + await new Promise(async resolve => { + const hocuspocus = await newHocuspocus(t, { + async onRequest({ instance }) { + t.is(instance, hocuspocus) + resolve('done') + }, + }) + + fetch(`${hocuspocus.server!.httpURL}/foobar`).catch(() => {}) + }) +}) diff --git a/tests/server/onStateless.ts b/tests/server/onStateless.ts new file mode 100644 index 0000000..93e6163 --- /dev/null +++ b/tests/server/onStateless.ts @@ -0,0 +1,101 @@ +import test from 'ava' +import type { onStatelessPayload } from '@hocuspocus/server' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('broadcast stateless message to all connections', async t => { + await new Promise(async resolve => { + const payloadToSend = 'STATELESS-MESSAGE' + const server = await newHocuspocus(t, { + onStateless: async ({ document }) => { + await document.broadcastStateless(payloadToSend) + }, + }) + + let count = 2 + const onStatelessCallback = (payload: string) => { + t.is(payload, payloadToSend) + count -= 1 + if (count === 0) { + t.pass() + resolve('done') + } + } + + newHocuspocusProvider(t, server, { onStateless: ({ payload }) => onStatelessCallback(payload) }) + const provider = newHocuspocusProvider(t, server, { + onStateless: ({ payload }) => onStatelessCallback(payload), + onSynced: () => { + provider.sendStateless(payloadToSend) + }, + }) + }) +}) + +test('send a stateless message to a specific connection', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + onStateless: async ({ connection }: onStatelessPayload) => { + connection.sendStateless('This is a specific message.') + }, + }) + + await newHocuspocusProvider(t, server, { + onStateless: async () => { + throw Error('The provider1 should not receive messages') + }, + }) + + const provider = await newHocuspocusProvider(t, server, { + onSynced: () => { + provider.sendStateless('Send stateless message, and then a stateless message is will be received') + }, + onStateless: () => { + t.pass() + resolve('done') + }, + }) + }) +}) + +test('calls the onStateless hook', async t => { + await new Promise(async resolve => { + const payloadToSend = 'STATELESS-MESSAGE' + class CustomExtension { + async onStateless({ payload }: onStatelessPayload) { + t.is(payload, payloadToSend) + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = await newHocuspocusProvider(t, server, { + onSynced: async () => { + provider.sendStateless(payloadToSend) + }, + }) + }) +}) + +test('the server actively sends a stateless message', async t => { + const payloadToSend = 'STATELESS-MESSAGE' + const server = await newHocuspocus(t) + + await new Promise(resolve => { + newHocuspocusProvider(t, server, { + onSynced: async () => { + server.documents.get('hocuspocus-test')?.broadcastStateless(payloadToSend) + }, + onStateless: async ({ payload }) => { + t.is(payload, payloadToSend) + t.pass() + resolve('done') + }, + }) + }) +}) diff --git a/tests/server/onStoreDocument.ts b/tests/server/onStoreDocument.ts new file mode 100644 index 0000000..f90d3d7 --- /dev/null +++ b/tests/server/onStoreDocument.ts @@ -0,0 +1,768 @@ +import type { Extension, onLoadDocumentPayload, onStoreDocumentPayload } from "@hocuspocus/server"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + newHocuspocusProviderWebsocket, + sleep, +} from "../utils/index.ts"; +import * as Y from "yjs"; + +test("calls the debouned onStoreDocument hook before the document is removed from memory", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + debounce: 3000, + async onStoreDocument() { + t.pass(); + resolve("done"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + + socket.destroy(); + }, + }); + }); +}); + +test("doesn’t remove the document from memory when there’s a new connection established during onStoreDocument is called", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument() { + return sleep(1000); + }, + }); + const socket = newHocuspocusProviderWebsocket(t, server); + const anotherSocket = newHocuspocusProviderWebsocket(t, server); + + newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + setTimeout(() => { + anotherSocket.connect(); + }, 100); + + setTimeout(() => { + t.is(server.getDocumentsCount(), 1); + resolve("done"); + }, 1100); + }, + }); + newHocuspocusProvider(t, server, { + websocketProvider: anotherSocket, + }); + }); +}); + +test("removes the document from memory when there’s no connection after onStoreDocument is called", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument() { + return sleep(1000); + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + provider.configuration.websocketProvider.destroy(); + provider.destroy(); + }, + onDestroy() { + // Check if the document is removed from memory … + setTimeout(() => { + t.is(server.getDocumentsCount(), 0); + + resolve("done"); + }, 1100); + }, + }); + }); +}); + +test("onStoreDocument callback receives document updates", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument({ document }: onStoreDocumentPayload) { + const value = document.getArray("foo").get(0); + t.is(value, "bar"); + + resolve("done"); + }, + }); + + const provider = newHocuspocusProvider(t, server); + + provider.on("synced", () => { + provider.document.getArray("foo").insert(0, ["bar"]); + }); + }); +}); + +test("debounces document changes for onStoreDocument hooks", async (t) => { + await new Promise(async (resolve) => { + let executedOnChange = 0; + let executedOnStoreDocument = 0; + + const hocuspocus = await newHocuspocus(t, { + debounce: 300, + async onChange() { + executedOnChange += 1; + }, + async onStoreDocument() { + executedOnStoreDocument += 1; + }, + }); + + const provider = newHocuspocusProvider(t, hocuspocus, { + onSynced() { + provider.document.getArray("foo").push(["foo"]); + provider.document.getArray("foo").push(["bar"]); + provider.document.getArray("foo").push(["barfoo"]); + provider.document.getArray("foo").push(["foobar"]); + provider.document.getArray("foo").push(["foofoo"]); + + setTimeout(async () => { + await hocuspocus.server!.destroy(); + + t.is(executedOnChange, 5); + t.is(executedOnStoreDocument, 1); + + resolve("done"); + }, 200); + }, + }); + }); +}); + +test("executes onStoreDocument callback from an extension", async (t) => { + await new Promise(async (resolve) => { + class CustomExtension { + async onStoreDocument({ document }: onStoreDocumentPayload) { + const value = document.getArray("foo").get(0); + t.is(value, "bar"); + + resolve("done"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new CustomExtension()], + }); + + const provider = newHocuspocusProvider(t, server); + + provider.on("synced", () => { + provider.document.getArray("foo").insert(0, ["bar"]); + }); + }); +}); + +test("stops when one of the onStoreDocument hooks throws an error", async (t) => { + await new Promise(async (resolve) => { + class BreakingTheChain { + async onStoreDocument() { + setTimeout(() => { + t.pass(); + resolve("done"); + }, 100); + + // Stop it! + throw new Error(); + } + } + + class NotExecuted { + async onStoreDocument() { + // This MUST NOT be executed. + + resolve("done"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new BreakingTheChain(), new NotExecuted()], + }); + + const provider = newHocuspocusProvider(t, server); + + provider.on("synced", () => { + provider.document.getArray("foo").insert(0, ["bar"]); + }); + }); +}); + +test("stops when one of the onStoreDocument hooks throws a non-Error value", async (t) => { + await new Promise(async (resolve) => { + class BreakingTheChain { + async onStoreDocument() { + setTimeout(() => { + t.pass(); + resolve("done"); + }, 100); + + // Throw a string instead of an Error object + throw "storage unavailable"; + } + } + + class NotExecuted { + async onStoreDocument() { + // This MUST NOT be executed. + t.fail("NotExecuted should not run"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new BreakingTheChain(), new NotExecuted()], + }); + + const provider = newHocuspocusProvider(t, server); + + provider.on("synced", () => { + provider.document.getArray("foo").insert(0, ["bar"]); + }); + }); +}); + +test("keeps document in memory when onStoreDocument fails", async (t) => { + let resolveProviderSynced!: () => void; + const providerSynced = new Promise((resolve) => { + resolveProviderSynced = resolve; + }); + let resolveDocumentChanged!: () => void; + const documentChanged = new Promise((resolve) => { + resolveDocumentChanged = resolve; + }); + let resolveStoreStarted!: () => void; + const storeStarted = new Promise((resolve) => { + resolveStoreStarted = resolve; + }); + const server = await newHocuspocus(t, { + debounce: 100, + onChange() { + resolveDocumentChanged(); + }, + async onStoreDocument() { + resolveStoreStarted(); + throw new Error("storage unavailable"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + resolveProviderSynced(); + }, + }); + + await providerSynced; + provider.document.getArray("foo").push(["bar"]); + await documentChanged; + socket.destroy(); + await storeStarted; + + const document = server.documents.get("hocuspocus-test"); + if (!document) { + throw new Error("Expected the document to remain loaded while storing"); + } + await document.saveMutex.waitForUnlock(); + + // Document must remain in memory since the store failed. + t.is(server.getDocumentsCount(), 1); +}); + +test("has the server instance", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument({ instance }) { + t.is(instance, server); + + resolve("done"); + }, + }); + + const provider = newHocuspocusProvider(t, server); + + provider.on("synced", () => { + provider.document.getArray("foo").insert(0, ["bar"]); + }); + }); +}); + +test("runs hooks in the given order", async (t) => { + await new Promise(async (resolve) => { + const triggered: string[] = []; + + class Running { + async onStoreDocument() { + triggered.push("one"); + } + } + + class BreakTheChain { + async onStoreDocument() { + triggered.push("two"); + throw Error(); + } + } + + class NotRunning { + async onStoreDocument() { + triggered.push("three"); + } + } + + const server = await newHocuspocus(t, { + extensions: [new Running(), new BreakTheChain(), new NotRunning()], + // lowest priority + async onStoreDocument() { + triggered.push("four"); + }, + async afterStoreDocument() { + t.fail(); // this shouldnt run + }, + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + provider.configuration.websocketProvider.destroy(); + provider.destroy(); + + setTimeout(() => { + t.deepEqual(triggered, ["one", "two"]); + + resolve(true); + }, 250); + }, + }); + }); +}); + +test("allows to overwrite the order of extension with a priority", async (t) => { + await new Promise(async (resolve) => { + const triggered: string[] = []; + + class Running { + async onStoreDocument() { + triggered.push("one"); + } + } + + class BreakTheChain { + async onStoreDocument() { + triggered.push("two"); + throw Error(); + } + } + + class NotRunning { + async onStoreDocument() { + triggered.push("three"); + } + } + + class HighPriority { + priority = 1000; + + async onStoreDocument() { + triggered.push("zero"); + } + } + + const server = await newHocuspocus(t, { + afterStoreDocument: async () => { + t.fail(); + }, + extensions: [ + new Running(), + new BreakTheChain(), + new NotRunning(), + new HighPriority(), + ], + }); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + provider.configuration.websocketProvider.destroy(); + provider.destroy(); + + setTimeout(() => { + t.deepEqual(triggered, ["zero", "one", "two"]); + + resolve(true); + }, 250); + }, + }); + }); +}); + +test("if a connection connects while another disconnects onStoreDocument is still running, onLoadDocument will be called after onStoreDocument finished", async (t) => { + await new Promise(async (testResolve) => { + let isStoredOnDb = false; + let loadCalls = 0; + + const server = await newHocuspocus(t, { + async onStoreDocument({ instance }) { + return new Promise((resolve) => { + setTimeout(() => { + isStoredOnDb = true; + resolve(true); + }, 200); + }); + }, + async onLoadDocument() { + return new Promise((resolve) => { + if (loadCalls > 0) { + t.true(isStoredOnDb); + testResolve(true); + } + + loadCalls += 1; + resolve(true); + }); + }, + }); + + const provider = newHocuspocusProvider(t, server); + provider.on("synced", () => { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + provider.configuration.websocketProvider.disconnect(); + + setTimeout(() => { + const provider2 = newHocuspocusProvider(t, server); + + provider2.on("synced", () => { + provider2.configuration.websocketProvider.disconnect(); + testResolve(true); + t.pass(); + }); + }, 50); + }); + }); +}); + +test("waits before calling onStoreDocument after the last user disconnects when configured", async (t) => { + await new Promise(async (resolve) => { + let startTime = 0; + const server = await newHocuspocus(t, { + unloadImmediately: false, + debounce: 500, + async onStoreDocument() { + const endTime = Date.now(); + if (startTime === 0) { + t.fail("startTime not set"); + } else if (endTime - startTime < 500) { + t.fail("did not wait 500ms to call onStoreDocument when closing"); + } else { + t.pass(); + } + resolve("done"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + startTime = Date.now(); + socket.destroy(); + }, + }); + }); +}); + +test("calls debounced onStoreDocument immediately after the last user disconnects when configured", async (t) => { + await new Promise(async (resolve) => { + let startTime = 0; + const server = await newHocuspocus(t, { + unloadImmediately: true, + debounce: 500, + async onStoreDocument() { + const endTime = Date.now(); + if (startTime === 0) { + t.fail("startTime not set"); + } else if (endTime - startTime < 500) { + t.pass(); + } else { + t.fail("did not call onStoreDocument immediately when closing"); + } + resolve("done"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + // Dummy change to trigger onStoreDocument + provider.document.getArray("foo").push(["foo"]); + startTime = Date.now(); + socket.destroy(); + }, + }); + }); +}); + +test("does not call the onStoreDocument hook if document is not changed after the last user disconnects", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onStoreDocument() { + t.fail( + "no need to call onStoreDocument if the document is not changed", + ); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onSynced() { + socket.destroy(); + }, + }); + + setTimeout(() => { + resolve("done"); + }, 200); + }); + t.pass(); +}); + +test("does not start a new onStoreDocument if there is already one running (should wait for the first one to finish)", async (t) => { + /* + If our storage backend takes more time than the debounce time to store the document, + we might end up in a situation where multiple onStoreDocument calls are running at the same time. + + Rough timeline: + + 1. ~0ms Client 1 connects + 2. ~10ms Client 1 makes change 1 (triggers debounced save) + 3. ~100ms Server starts saving change 1 (debounced) + 4. ~200ms Client 1 makes change 2 (triggers debounced save) + 6. ~510ms Server finishes saving change 1. + 6. ~511ms Server starts saving change 2 (debounced) + 7. ~1111ms Server finishes saving change 2. + */ + + await new Promise(async (resolve) => { + let started = 0 + let finished = 0 + const server = await newHocuspocus(t, { + debounce: 100, + async onStoreDocument() { + if (started === 1) { + // This is the second call + t.is(finished, 1, "the first call must have finished before starting the second"); + resolve("done"); + } else { + started++ + await sleep(500) // Simulate long save + finished++ + } + }, + }); + + const socket1 = newHocuspocusProviderWebsocket(t, server); + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket1, + async onSynced() { + // Change 1 + provider1.document.getArray("foo").push(["foo"]); + setTimeout(() => { + // Change 2 + provider1.document.getArray("foo").push(["bar"]); + socket1.destroy(); + }, 200) + }, + }); + }); +}) + +test("triggers unload only after finishing with a save in progress", async (t) => { + /* + Rough timeline: + + 1. ~0ms Client 1 connects + 2. ~10ms Client 1 makes change 1 (triggers debounced save) + 3. ~1000ms Server starts saving change 1 (debounced) + 4. ~1100ms Client 1 disconnects. Document is not unloaded because save is in progress. + 5. ~1500ms Server finishes saving change 1. Document is unloaded + */ + await new Promise(async (resolve) => { + const start = Date.now() + let saveStarted = false + let saveFinished = false + const server = await newHocuspocus(t, { + debounce: 1000, + extensions: [{ + async onStoreDocument() { + saveStarted = true + await sleep(500) // Add pause to simulate long save + saveFinished = true + }, + async afterUnloadDocument(data) { + t.deepEqual(saveFinished, true, "Unload should occur only after save was finished"); + t.pass() + resolve("done"); + }, + }], + }); + const socket1 = newHocuspocusProviderWebsocket(t, server); + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket1, + async onSynced() { + provider1.document.getArray("foo").push(["foo"]); + setTimeout(() => { // Wait for sending changes + socket1.destroy(); + }, 1100) + }, + }); + }) +}) + +test("does not trigger unload prematurely when a save is in progress (unloadImmediately=true)", async (t) => { + /* + Rough timeline: + + 1. ~0ms Client 1 connects + 2. ~10ms Client 1 makes change 1 (triggers debounced save) + 3. ~1000ms Server starts saving change 1 (debounced) + 4. ~1100ms Client 1 makes change 2 (triggers debounced save) + 5. ~1300ms Client 1 disconnects, triggering immediate save of change 2 + 6. ~1500ms Server finishes saving change 1. Does not unload the document, because change 2 is still being saved. + 7. ~1600ms Client 2 connects, loads document + 9. ~1800ms Server finishes saving change 2. + 10. ~2100ms We verify that Client 2 sees both changes. + */ + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + debounce: 1000, + extensions: [new SlowInMemoryStorage()], + }); + + const socket1 = newHocuspocusProviderWebsocket(t, server); + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket1, + + async onSynced() { + provider1.document.getArray("foo").push(["foo"]); + setTimeout(() => { + provider1.document.getArray("foo").push(["bar"]); + setTimeout(() => { // Wait for sending changes + socket1.destroy(); + }, 200) + }, 1100) + }, + }); + + setTimeout(() => { + const socket2 = newHocuspocusProviderWebsocket(t, server); + const provider2 = newHocuspocusProvider(t, server, { + websocketProvider: socket2, + + async onSynced() { + setTimeout(() => { + const value = provider2.document.getArray("foo").toArray() + t.deepEqual(value, ["foo", "bar"], "Client 2 should see both changes"); + t.pass() + resolve("done"); + }, 500) + }, + }); + }, 1600) + }); +}); + +test("Does not unload prematurely when a debounced save is pending (unloadImmediately=false)", async (t) => { + /* + + Rough timeline: + + 1. ~0ms Client 1 connects + 2. ~10ms Client 1 makes a change 1 (triggers debounced save) + 3. ~1000ms Server starts saving change 1 (debounced) + 4. ~1100ms Client 1 makes change 2 (triggers debounced save) + 5. ~1300ms Client 1 disconnects, while change 2 is still being debounced + 6. ~1500ms Server finishes saving change 1. Does not unload the document, because change 2 is still being debounced. + 7. ~1600ms Client 2 connects + 8. ~2000ms Server starts saving change 2 (debounced) + 9. ~2500ms Server finishes saving change 2 + 10. ~3000ms We verify that Client 2 sees both changes. + + */ + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + debounce: 1000, + unloadImmediately: false, + extensions: [new SlowInMemoryStorage()], + }); + + const socket1 = newHocuspocusProviderWebsocket(t, server); + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket1, + + async onSynced() { + provider1.document.getArray("foo").push(["foo"]); + }, + }); + setTimeout(() => { + provider1.document.getArray("foo").push(["bar"]); + setTimeout(() => { // Wait for sending changes + socket1.destroy(); + }, 200) + }, 1100) + + setTimeout(() => { + const socket2 = newHocuspocusProviderWebsocket(t, server); + const provider2 = newHocuspocusProvider(t, server, { + websocketProvider: socket2, + async onSynced() { + setTimeout(() => { + t.deepEqual(getCurrentValue(provider2.document), ["foo", "bar"], "Client 2 should see both changes"); + t.pass() + resolve("done"); + }, 1500) + }, + }); + }, 1600) + + + }); +}); + +const getCurrentValue = (doc: Y.Doc) => doc.getArray("foo").toArray() + +class SlowInMemoryStorage implements Extension { + state: Uint8Array | null = null + async onStoreDocument(data: onStoreDocumentPayload) { + const stateToSave = Y.encodeStateAsUpdate(data.document) // Capture state to save immediately + await sleep(500) // Add pause to simulate long save + this.state = stateToSave + } + async onLoadDocument(data: onLoadDocumentPayload) { + if (this.state) { + Y.applyUpdate(data.document, this.state) + } + } +} diff --git a/tests/server/onTokenSync.ts b/tests/server/onTokenSync.ts new file mode 100644 index 0000000..3766492 --- /dev/null +++ b/tests/server/onTokenSync.ts @@ -0,0 +1,365 @@ +import test from 'ava' +import type { onAuthenticatePayload, onTokenSyncPayload } from '@hocuspocus/server' +import { Unauthorized } from '@hocuspocus/common' +import { + newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket, sleep, +} from '../utils/index.ts' + +// ============================================================================ +// PROVIDER SEND TOKEN TESTS +// Provider calls sendToken() after onAuthenticated() +// ============================================================================ + +test('provider sendToken: onTokenSync receives correct token from provider after authentication', async t => { + await new Promise(async resolve => { + const expectedToken = 'UPDATED-TOKEN' + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true // Allow initial auth + }, + async onTokenSync({ token }: onTokenSyncPayload) { + t.is(token, expectedToken) + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'INITIAL-TOKEN', // Initial token for auth + onAuthenticated() { + // Update the provider's token before sending + provider.configuration.token = expectedToken + // Provider sends updated token after authentication + provider.sendToken() // This will send the current token + }, + }) + }) +}) + +test('provider sendToken: executes onTokenSync from custom extension when provider sends token', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onAuthenticate() { + return true + } + + async onTokenSync({ token }: onTokenSyncPayload) { + t.is(token, 'SUPER-SECRET-TOKEN') + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onAuthenticated() { + provider.sendToken() + }, + }) + }) +}) + +test('provider sendToken: onTokenSync has access to full payload when provider sends token', async t => { + const mockContext = { user: 123 } + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return mockContext + }, + async onTokenSync({ context, connection, documentName, token }: onTokenSyncPayload) { + t.deepEqual(context, mockContext) + t.truthy(connection) + t.is(documentName, 'hocuspocus-test') + t.is(token, 'SUPER-SECRET-TOKEN') + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onAuthenticated() { + provider.sendToken() + }, + }) + }) +}) + +test('provider sendToken: onTokenSync works with multiple providers sending tokens', async t => { + const doc1 = 'document1' + const doc2 = 'document2' + const token1 = 'TOKEN-1' + const token2 = 'TOKEN-2' + let completedCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true // Allow initial auth + }, + async onTokenSync({ documentName, token }: onTokenSyncPayload) { + // Direct verification based on document name + if (documentName === doc1) { + t.is(token, token1) + } else if (documentName === doc2) { + t.is(token, token2) + } + + completedCount++ + if (completedCount === 2) { + resolve('done') + } + }, + }) + + const socket = newHocuspocusProviderWebsocket(t, server) + + // Create two providers for different documents + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: token1, + name: doc1, + onAuthenticated() { + provider1.sendToken() + }, + }) + + const provider2 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: token2, + name: doc2, + onAuthenticated() { + provider2.sendToken() + }, + }) + }) +}) + +// ============================================================================ +// SERVER REQUEST TOKEN TESTS +// Server calls connection.requestToken() after onAuthenticate() completes +// ============================================================================ + +test('server requestToken: executes onTokenSync when server requests token after authentication', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true + }, + async onTokenSync({ token }: onTokenSyncPayload) { + t.is(token, 'SUPER-SECRET-TOKEN') + t.pass() + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + }) + + // Wait for provider to be authenticated to be able to find the connection + await sleep(100) + + const document = server.documents.get('hocuspocus-test') + if (document) { + const connection = document.connections.keys().next().value + if (connection) { + connection.requestToken() + } + } + }) +}) + +test('server requestToken: executes onTokenSync from custom extension when server requests token', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onAuthenticate() { + return true + } + + async onTokenSync({ token }: onTokenSyncPayload) { + t.is(token, 'SUPER-SECRET-TOKEN') + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + }) + + await sleep(100) + + const document = server.documents.get('hocuspocus-test') + if (document) { + const connection = document.connections.keys().next().value + if (connection) { + connection.requestToken() + } + } + }) +}) + +test('server requestToken: onTokenSync receives correct token when server requests it', async t => { + await new Promise(async resolve => { + const expectedToken = 'UPDATED-TOKEN' + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true + }, + async onTokenSync({ token }: onTokenSyncPayload) { + t.is(token, expectedToken) + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'INITIAL-TOKEN', + onAuthenticated() { + // Update the provider's token after connection + provider.configuration.token = expectedToken + }, + }) + + await sleep(100) + + // Now trigger token sync request from server + const document = server.documents.get('hocuspocus-test') + if (document) { + const connection = document.connections.keys().next().value + if (connection) { + connection.requestToken() + } + } + }) +}) + +test('server requestToken: onTokenSync works with multiple documents when server requests tokens', async t => { + const doc1 = 'document1' + const doc2 = 'document2' + const token1 = 'TOKEN-1' + const token2 = 'TOKEN-2' + let completedCount = 0 + + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true // Allow initial auth + }, + async onTokenSync({ documentName, token }: onTokenSyncPayload) { + // Direct verification based on document name + if (documentName === doc1) { + t.is(token, token1) + } else if (documentName === doc2) { + t.is(token, token2) + } + + completedCount++ + if (completedCount === 2) { + resolve('done') + } + }, + }) + + const socket = newHocuspocusProviderWebsocket(t, server) + + // Create two providers for different documents + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: token1, + name: doc1, + }) + + const provider2 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: token2, + name: doc2, + }) + + // Wait for both authentications to complete + await sleep(100) + + // Now trigger token sync requests from server for both documents + const doc1Connection = server.documents.get(doc1)?.connections.keys().next().value + const doc2Connection = server.documents.get(doc2)?.connections.keys().next().value + + if (doc1Connection) doc1Connection.requestToken() + if (doc2Connection) doc2Connection.requestToken() + }) +}) + +test('server requestToken: onTokenSync works with readonly connections when server requests token', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate({ connectionConfig }: onAuthenticatePayload) { + connectionConfig.readOnly = true + return true + }, + async onTokenSync({ connection }: onTokenSyncPayload) { + t.is(connection.readOnly, true) + resolve('done') + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'readonly-token', + }) + + // Wait for authentication to complete first + await sleep(100) + + // Now trigger token sync request from server + const document = server.documents.get('hocuspocus-test') + if (document) { + const connection = document.connections.keys().next().value + if (connection) { + connection.requestToken() + } + } + }) +}) + +test('server requestToken: failure of onTokenSync should close the connection', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true // Allow initial auth + }, + async onTokenSync() { + throw new Error() + }, + }) + + const provider = newHocuspocusProvider(t, server, { + token: 'SUPER-SECRET-TOKEN', + onClose({ event }) { + t.is(event.reason, Unauthorized.reason) + t.pass() + resolve('done') + }, + }) + + await sleep(100) + + const document = server.documents.get('hocuspocus-test') + if (document) { + const connection = document.connections.keys().next().value + if (connection) { + connection.requestToken() + } + } + }) +}) diff --git a/tests/server/onUpgrade.ts b/tests/server/onUpgrade.ts new file mode 100644 index 0000000..d221e5f --- /dev/null +++ b/tests/server/onUpgrade.ts @@ -0,0 +1,90 @@ +import test from 'ava' +import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts' + +test('executes the onUpgrade callback', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onUpgrade() { + t.pass() + resolve('done') + }, + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('executes the onUpgrade callback from an extension', async t => { + await new Promise(async resolve => { + class CustomExtension { + async onUpgrade() { + t.pass() + resolve('done') + } + } + + const server = await newHocuspocus(t, { + extensions: [ + new CustomExtension(), + ], + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('has the server instance', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + + async onUpgrade({ instance }) { + t.is(instance, server) + resolve('done') + }, + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('has the request', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + + async onUpgrade({ request }) { + t.is(request.url, '/') + resolve('done') + }, + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('has the socket', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + + async onUpgrade({ socket }) { + t.truthy(socket) + resolve('done') + }, + }) + + newHocuspocusProvider(t, server) + }) +}) + +test('has the head', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + + async onUpgrade({ head }) { + t.truthy(head) + resolve('done') + }, + }) + + newHocuspocusProvider(t, server) + }) +}) diff --git a/tests/server/openDirectConnection.ts b/tests/server/openDirectConnection.ts new file mode 100644 index 0000000..dd5ea33 --- /dev/null +++ b/tests/server/openDirectConnection.ts @@ -0,0 +1,466 @@ +import { TiptapTransformer } from "@hocuspocus/transformer"; +import test from "ava"; +import * as Y from "yjs"; +import { newHocuspocus, newHocuspocusProvider, sleep } from "../utils/index.ts"; + +test("rejects empty document name via direct connection", async (t) => { + const server = await newHocuspocus(t); + + await t.throwsAsync(() => server.openDirectConnection(""), { + message: "Document name must not be empty", + }); + + t.is(server.getDocumentsCount(), 0); +}); + +test("rejects whitespace-only document name via direct connection", async (t) => { + const server = await newHocuspocus(t); + + await t.throwsAsync(() => server.openDirectConnection(" "), { + message: "Document name must not be empty", + }); + + t.is(server.getDocumentsCount(), 0); +}); + +test("direct connection prevents document from being removed from memory", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + await server.openDirectConnection("hocuspocus-test"); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.configuration.websocketProvider.destroy(); + provider.destroy(); + + sleep(server.configuration.debounce + 50).then(() => { + t.is(server.getDocumentsCount(), 1); + resolve("done"); + }); + }, + }); + }); +}); +test("direct connection works even if provider is connected", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + const provider = newHocuspocusProvider(t, server, { + onSynced() { + provider.document.getMap("config").set("a", "valueFromProvider"); + }, + }); + + await sleep(150); + + const directConnection = + await server.openDirectConnection("hocuspocus-test"); + await directConnection.transact((doc) => { + t.is("valueFromProvider", String(doc.getMap("config").get("a"))); + doc.getMap("config").set("b", "valueFromServerDirectConnection"); + }); + + await sleep(100); + t.is( + "valueFromServerDirectConnection", + String(provider.document.getMap("config").get("b")), + ); + + resolve(1); + t.pass(); + }); +}); + +test("direct connection can apply yjsUpdate", async (t) => { + const server = await newHocuspocus(t); + const expectedContent = "Example Paragraph"; + let resolveProviderSynced!: () => void; + const providerSynced = new Promise((resolve) => { + resolveProviderSynced = resolve; + }); + const provider = newHocuspocusProvider(t, server, { + onSynced() { + resolveProviderSynced(); + }, + }); + + await providerSynced; + const fragment = provider.document.getXmlFragment("default"); + t.is(fragment.toJSON(), ""); + + let resolveUpdateApplied!: () => void; + const updateApplied = new Promise((resolve) => { + resolveUpdateApplied = resolve; + }); + const observer = () => { + if (fragment.toJSON() === expectedContent) { + fragment.unobserveDeep(observer); + resolveUpdateApplied(); + } + }; + fragment.observeDeep(observer); + + const directConnection = + await server.openDirectConnection("hocuspocus-test"); + await directConnection.transact((doc) => { + Y.applyUpdate( + doc, + Y.encodeStateAsUpdate( + TiptapTransformer.toYdoc({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Example Paragraph", + }, + ], + }, + ], + }), + ), + ); + }); + + await updateApplied; + t.is(fragment.toJSON(), expectedContent); + await directConnection.disconnect(); +}); + +test("direct connection can transact", async (t) => { + const server = await newHocuspocus(t); + + const direct = await server.openDirectConnection("hocuspocus-test"); + + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + + t.is(direct.document?.getArray("test").toJSON()[0], "value"); +}); + +test("direct connection cannot transact once closed", async (t) => { + const server = await newHocuspocus(t); + + const direct = await server.openDirectConnection("hocuspocus-test"); + await direct.disconnect(); + + try { + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + t.fail( + "DirectConnection should throw an error when transacting on closed connection", + ); + } catch (err) { + if (err instanceof Error && err.message === "direct connection closed") { + t.pass(); + } else { + t.fail("unknown error"); + } + } +}); + +test("if a direct connection closes, the document should be unloaded if there is no other connection left", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t); + + const direct = await server.openDirectConnection("hocuspocus-test1"); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + + await direct.disconnect(); + + t.is(server.getConnectionsCount(), 0); + t.is(server.getDocumentsCount(), 0); + resolve("done"); + }); +}); + +test("direct connection transact awaits until onStoreDocument has finished", async (t) => { + let onStoreDocumentFinished = false; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onStoreDocument: async () => { + onStoreDocumentFinished = false; + await sleep(200); + onStoreDocumentFinished = true; + }, + }); + + const direct = await server.openDirectConnection("hocuspocus-test2"); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + + t.is(onStoreDocumentFinished, false); + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + + await direct.disconnect(); + t.is(onStoreDocumentFinished, true); + + t.is(server.getConnectionsCount(), 0); + t.is(server.getDocumentsCount(), 0); + t.is(onStoreDocumentFinished, true); + resolve("done"); + }); +}); + +test("direct connection transact awaits until onStoreDocument has finished, even if unloadImmediately=false", async (t) => { + let onStoreDocumentFinished = false; + let directConnDisconnecting = false; + let storedAfterDisconnect = false; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + unloadImmediately: false, + onStoreDocument: async () => { + onStoreDocumentFinished = false; + await sleep(200); + onStoreDocumentFinished = true; + + if (directConnDisconnecting) { + storedAfterDisconnect = true; + } + }, + afterUnloadDocument: async (data) => { + if (!storedAfterDisconnect) { + t.fail("this shouldnt be called"); + } + }, + }); + + const direct = await server.openDirectConnection("hocuspocus-test"); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + + t.is(onStoreDocumentFinished, false); + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + + const provider = newHocuspocusProvider(t, server); + provider.document.getMap("aaa").set("bb", "b"); + provider.disconnect(); + provider.configuration.websocketProvider.disconnect(); + + await sleep(100); + + directConnDisconnecting = true; + await direct.disconnect(); + t.is(onStoreDocumentFinished, true); + + t.is(server.getConnectionsCount(), 0); + + t.is(storedAfterDisconnect, true); + + resolve("done"); + }); +}); + +test("does not unload document if an earlierly started onStoreDocument is still running", async (t) => { + let onStoreDocumentStarted = 0; + let onStoreDocumentFinished = 0; + + const server = await newHocuspocus(t, { + unloadImmediately: false, + debounce: 100, + onStoreDocument: async () => { + onStoreDocumentStarted++; + if (onStoreDocumentStarted === 1) { + // Simulate a long running onStoreDocument for the first debounced save + await sleep(500); + } + onStoreDocumentFinished++; + }, + afterUnloadDocument: async (data) => {}, + }); + + // Trigger a change, which will start a debounced onStoreDocument after 100ms + const provider = newHocuspocusProvider(t, server); + provider.document.getMap("aaa").set("bb", "b"); + + await new Promise(async (resolve) => { + provider.on("synced", resolve); + + if (!provider.unsyncedChanges) resolve(""); + }); + + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + + // Wait for the debounced onStoreDocument to start + await sleep(110); + t.is(onStoreDocumentStarted, 1); + t.is(onStoreDocumentFinished, 0); + + // Open direct connection to prevent document from being unloaded + const direct = await server.openDirectConnection("hocuspocus-test"); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 2); + + // Close the websocket client + provider.disconnect(); + provider.configuration.websocketProvider.disconnect(); + await sleep(50); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + t.is(onStoreDocumentStarted, 1); + t.is(onStoreDocumentFinished, 0); + + direct.disconnect(); + await sleep(50); + // Another save must not start before the first one has finished + t.is(onStoreDocumentStarted, 1); + t.is(onStoreDocumentFinished, 0); + // Document must not be unloaded yet, because the first onStoreDocument is still running + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 0); + + // Wait enough time to be sure the onStoreDocument has finished and ensure that the document was eventually unloaded + await sleep(500); + + // The second onStoreDocument triggered by direct.disconnect must have started and finished now + t.is(onStoreDocumentStarted, 2); + t.is(onStoreDocumentFinished, 2); + // The document must have been unloaded now as well + t.is(server.getDocumentsCount(), 0); +}); + +test("creating a websocket connection after transact but before debounce interval doesnt create different docs", async (t) => { + let onStoreDocumentFinished = false; + let disconnected = false; + let testDone = false; + + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + onStoreDocument: async () => { + onStoreDocumentFinished = false; + await sleep(200); + onStoreDocumentFinished = true; + }, + async afterUnloadDocument(data) { + if (disconnected && !testDone) { + t.fail("must not be called"); + } + }, + }); + + const direct = await server.openDirectConnection("hocuspocus-test"); + t.is(server.getDocumentsCount(), 1); + t.is(server.getConnectionsCount(), 1); + + t.is(onStoreDocumentFinished, false); + await direct.transact((document) => { + document.transact(() => { + document.getArray("test").insert(0, ["value"]); + }, "testOrigin"); + }); + + await direct.disconnect(); + t.is(onStoreDocumentFinished, true); + disconnected = true; + + t.is(server.getConnectionsCount(), 0); + t.is(server.getDocumentsCount(), 0); + t.is(onStoreDocumentFinished, true); + + const provider = newHocuspocusProvider(t, server); + + await sleep(server.configuration.debounce * 2); + + testDone = true; + resolve("done"); + }); +}); + +test("direct connection passes context", async (t) => { + return new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onChange(x) { + t.is(x.context.x, 123); + t.pass(); + resolve(); + }, + }); + + const direct = await server.openDirectConnection("hocuspocus-test", { + x: 123, + }); + + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + + t.is(direct.document?.getArray("test").toJSON()[0], "value"); + }); +}); + +test("disconnect({ unloadImmediately: false }) keeps the document warm and coalesces stores", async (t) => { + let storeCount = 0; + + const server = await newHocuspocus(t, { + debounce: 100, + maxDebounce: 500, + onStoreDocument: async () => { + storeCount += 1; + }, + }); + + const first = await server.openDirectConnection("hocuspocus-test"); + await first.transact((document) => { + document.getArray("test").insert(0, ["a"]); + }); + await first.disconnect({ unloadImmediately: false }); + + // The document stays in memory and nothing is persisted yet. + t.is(server.getConnectionsCount(), 0); + t.is(server.getDocumentsCount(), 1); + t.is(storeCount, 0); + + // A follow-up direct connection reuses the warm document. + const second = await server.openDirectConnection("hocuspocus-test"); + t.is(server.getDocumentsCount(), 1); + await second.transact((document) => { + document.getArray("test").insert(1, ["b"]); + }); + await second.disconnect({ unloadImmediately: false }); + + t.is(server.getDocumentsCount(), 1); + t.is(storeCount, 0); + + // After the debounce window the store flushes once (coalesced) and the + // document is unloaded. + await sleep(server.configuration.maxDebounce + server.configuration.debounce + 100); + t.is(storeCount, 1); + t.is(server.getDocumentsCount(), 0); +}); + +test("disconnect() without options still persists and unloads immediately", async (t) => { + let storeCount = 0; + + const server = await newHocuspocus(t, { + onStoreDocument: async () => { + storeCount += 1; + }, + }); + + const direct = await server.openDirectConnection("hocuspocus-test"); + await direct.transact((document) => { + document.getArray("test").insert(0, ["value"]); + }); + await direct.disconnect(); + + t.is(storeCount, 1); + t.is(server.getConnectionsCount(), 0); + t.is(server.getDocumentsCount(), 0); +}); diff --git a/tests/server/providerVersion.ts b/tests/server/providerVersion.ts new file mode 100644 index 0000000..758acd4 --- /dev/null +++ b/tests/server/providerVersion.ts @@ -0,0 +1,101 @@ +import type { + connectedPayload, + onAuthenticatePayload, + onConnectPayload, +} from "@hocuspocus/server"; +import test from "ava"; +import { + newHocuspocus, + newHocuspocusProvider, + newHocuspocusProviderWebsocket, +} from "../utils/index.ts"; + +test("onAuthenticate receives providerVersion", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ providerVersion }: onAuthenticatePayload) { + t.is(typeof providerVersion, "string"); + t.not(providerVersion, null); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "test-token", + }); + }); +}); + +test("onConnect receives providerVersion", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onConnect({ providerVersion }: onConnectPayload) { + t.is(typeof providerVersion, "string"); + t.not(providerVersion, null); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "test-token", + }); + }); +}); + +test("connected receives providerVersion and it is set on the connection", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async connected({ + providerVersion, + connection, + }: connectedPayload) { + t.is(typeof providerVersion, "string"); + t.not(providerVersion, null); + t.is(connection.providerVersion, providerVersion); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "test-token", + }); + }); +}); + +test("providerVersion is a non-empty string", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ providerVersion }: onAuthenticatePayload) { + t.is(typeof providerVersion, "string"); + t.truthy(providerVersion!.length > 0); + resolve("done"); + }, + }); + + newHocuspocusProvider(t, server, { + token: "test-token", + }); + }); +}); + +test("providerVersion is the same across multiplexed documents", async (t) => { + await new Promise(async (resolve) => { + const versions: string[] = []; + + const server = await newHocuspocus(t, { + async onAuthenticate({ providerVersion }: onAuthenticatePayload) { + versions.push(providerVersion!); + if (versions.length === 2) { + t.is(versions[0], versions[1]); + t.not(versions[0], null); + resolve("done"); + } + }, + }); + + const ws = newHocuspocusProviderWebsocket(t, server); + + newHocuspocusProvider(t, server, { name: "doc1", token: "t1" }, {}, ws); + newHocuspocusProvider(t, server, { name: "doc2", token: "t2" }, {}, ws); + }); +}); diff --git a/tests/server/sessionAwareness.ts b/tests/server/sessionAwareness.ts new file mode 100644 index 0000000..7ae2d95 --- /dev/null +++ b/tests/server/sessionAwareness.ts @@ -0,0 +1,329 @@ +import { parseRoutingKey } from "@hocuspocus/common"; +import type { onAuthenticatePayload, connectedPayload } from "@hocuspocus/server"; +import test from "ava"; +import { readVarString, createDecoder } from "lib0/decoding"; +import { + newHocuspocus, + newHocuspocusProvider, + newHocuspocusProviderWebsocket, +} from "../utils/index.ts"; +import { retryableAssertion } from "../utils/retryableAssertion.ts"; + +test("sessionAwareness: two providers with same doc name both connect successfully", async (t) => { + await new Promise(async (resolve) => { + let connectedCount = 0; + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider1 = newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "token-1", + name: "shared-doc", + sessionAwareness: true, + onAuthenticated() { + connectedCount++; + if (connectedCount === 2) { + resolve("done"); + } + }, + }, + ); + + const provider2 = newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "token-2", + name: "shared-doc", + sessionAwareness: true, + onAuthenticated() { + connectedCount++; + if (connectedCount === 2) { + resolve("done"); + } + }, + }, + ); + + t.truthy(provider1); + t.truthy(provider2); + }); +}); + +test("sessionAwareness: auth failure isolation - provider A fails, provider B succeeds", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate({ token }: onAuthenticatePayload) { + if (token === "bad-token") { + throw new Error("unauthorized"); + } + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const providerFail = newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "bad-token", + name: "shared-doc", + sessionAwareness: true, + onAuthenticated() { + t.fail("providerFail should not authenticate"); + }, + }, + ); + + const providerOK = newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "good-token", + name: "shared-doc", + sessionAwareness: true, + onAuthenticationFailed() { + t.fail("providerOK should not fail auth"); + }, + }, + ); + + await retryableAssertion(t, (tt) => { + tt.is(providerFail.isAuthenticated, false); + tt.is(providerOK.isAuthenticated, true); + tt.is(server.getDocumentsCount(), 1); + }); +}); + +test("sessionAwareness: false - two providers with same name on same socket throws when first is authenticated", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider1 = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "token", + name: "same-doc", + sessionAwareness: false, + }); + + await retryableAssertion(t, (tt) => { + tt.is(provider1.isAuthenticated, true); + }); + + // Now that provider1 is authenticated, attaching a second with the same name should throw + t.throws(() => { + newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "token", + name: "same-doc", + sessionAwareness: false, + }); + }); +}); + +test("sessionAwareness: connection has correct sessionId", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + async connected({ connection }: connectedPayload) { + t.truthy(connection.sessionId); + t.is(typeof connection.sessionId, "string"); + t.truthy(connection.sessionId!.length > 0); + resolve("done"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "test-token", + name: "session-doc", + sessionAwareness: true, + }); + }); +}); + +test("sessionAwareness: connection has correct providerVersion", async (t) => { + await new Promise(async (resolve) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + async connected({ connection }: connectedPayload) { + t.is(typeof connection.providerVersion, "string"); + t.truthy(connection.providerVersion!.length > 0); + resolve("done"); + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "test-token", + name: "session-doc", + sessionAwareness: true, + }); + }); +}); + +test("sessionAwareness: all outgoing messages from provider include sessionId in routing key", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const sentMessages: Uint8Array[] = []; + const originalSend = socket.webSocket!.send.bind(socket.webSocket!); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "test-token", + name: "session-doc", + sessionAwareness: true, + }); + + await retryableAssertion(t, (tt) => { + tt.is(provider.isSynced, true); + }); + + // Monkey-patch send to capture outgoing messages + socket.webSocket!.send = (data: any) => { + if (data instanceof Uint8Array || data instanceof ArrayBuffer) { + sentMessages.push(new Uint8Array(data)); + } + originalSend(data); + }; + + // Trigger a document update which causes the provider to send an UpdateMessage + provider.document.getMap("test").set("key", "value"); + + await retryableAssertion(t, (tt) => { + tt.true(sentMessages.length > 0, "should have captured outgoing messages"); + + for (const msg of sentMessages) { + const decoder = createDecoder(msg); + const routingKey = readVarString(decoder); + const { documentName, sessionId } = parseRoutingKey(routingKey); + tt.is(documentName, "session-doc", `message routing key should contain correct document name, got: ${routingKey}`); + tt.truthy(sessionId, `every outgoing message should include a sessionId in the routing key, got: ${routingKey}`); + } + }); +}); + +test("sessionAwareness: SyncStep2 reply includes sessionId in routing key", async (t) => { + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + token: "test-token", + name: "sync-reply-doc", + sessionAwareness: true, + }); + + await retryableAssertion(t, (tt) => { + tt.is(provider.isSynced, true); + }); + + // Capture all outgoing messages + const sentMessages: Uint8Array[] = []; + const originalSend = socket.webSocket!.send.bind(socket.webSocket!); + socket.webSocket!.send = (data: any) => { + if (data instanceof Uint8Array || data instanceof ArrayBuffer) { + sentMessages.push(new Uint8Array(data)); + } + originalSend(data); + }; + + // Force a sync which triggers SyncStep1 -> server replies with SyncStep1 -> provider replies with SyncStep2 + provider.forceSync(); + + await retryableAssertion(t, (tt) => { + // We expect at least the SyncStep1 from forceSync plus the SyncStep2 reply + tt.true(sentMessages.length >= 2, `should have at least 2 messages, got ${sentMessages.length}`); + + for (const msg of sentMessages) { + const decoder = createDecoder(msg); + const routingKey = readVarString(decoder); + const { sessionId } = parseRoutingKey(routingKey); + tt.truthy(sessionId, `message should include sessionId, got routing key: ${routingKey}`); + } + }); +}); + +test("sessionAwareness: providers with different doc names still work without sessionAwareness", async (t) => { + await new Promise(async (resolve) => { + let connectedCount = 0; + + const server = await newHocuspocus(t, { + async onAuthenticate() { + return true; + }, + }); + + const socket = newHocuspocusProviderWebsocket(t, server); + + newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "token-1", + name: "doc-1", + onAuthenticated() { + connectedCount++; + if (connectedCount === 2) { + resolve("done"); + } + }, + }, + ); + + newHocuspocusProvider( + t, + server, + { + websocketProvider: socket, + token: "token-2", + name: "doc-2", + onAuthenticated() { + connectedCount++; + if (connectedCount === 2) { + resolve("done"); + } + }, + }, + ); + + t.pass(); + }); +}); diff --git a/tests/server/unauthenticatedQueueLimit.ts b/tests/server/unauthenticatedQueueLimit.ts new file mode 100644 index 0000000..7468e28 --- /dev/null +++ b/tests/server/unauthenticatedQueueLimit.ts @@ -0,0 +1,227 @@ +import test from 'ava' +import * as encoding from 'lib0/encoding' +import { writeAuthentication } from '@hocuspocus/common' +import { newHocuspocus } from '../utils/index.ts' + +// Wire-level MessageType values (mirrors packages/server/src/types.ts). +const MessageType = { + Auth: 2, + Stateless: 5, +} + +const buildStatelessFrame = (documentName: string, payloadSize: number): Uint8Array => { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, MessageType.Stateless) + encoding.writeVarString(encoder, 'A'.repeat(payloadSize)) + return encoding.toUint8Array(encoder) +} + +const buildAuthFrame = (documentName: string, token: string): Uint8Array => { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, MessageType.Auth) + writeAuthentication(encoder, token) + return encoding.toUint8Array(encoder) +} + +const buildCloseFrame = (documentName: string): Uint8Array => { + const encoder = encoding.createEncoder() + encoding.writeVarString(encoder, documentName) + encoding.writeVarUint(encoder, 7) // MessageType.CLOSE + return encoding.toUint8Array(encoder) +} + +const openRawSocket = (url: string): Promise => new Promise((resolve, reject) => { + const ws = new WebSocket(url) + ws.binaryType = 'arraybuffer' + ws.addEventListener('open', () => resolve(ws), { once: true }) + ws.addEventListener('error', (event) => reject(event), { once: true }) +}) + +// Wait for the server to close the socket, returning the close code. +const waitForClose = (ws: WebSocket, timeoutMs = 5000): Promise => new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('connection was not closed by the server')), timeoutMs) + ws.addEventListener('close', (event) => { + clearTimeout(timer) + resolve(event.code) + }, { once: true }) +}) + +test('closes an unauthenticated connection that exceeds the pre-auth message-count limit', async t => { + const server = await newHocuspocus(t, { + maxUnauthenticatedQueueMessages: 5, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + const closed = waitForClose(attacker) + + // Flood many small non-Auth frames before authenticating. The server must + // stop queuing and close the connection once the count limit is exceeded. + for (let i = 0; i < 50; i += 1) { + attacker.send(buildStatelessFrame('queue-count-doc', 16)) + } + + const code = await closed + t.is(code, 4205, 'connection should be closed with ResetConnection (4205)') +}) + +test('closes an unauthenticated connection that exceeds the pre-auth byte limit', async t => { + const server = await newHocuspocus(t, { + // 256 KB total budget across the unauthenticated queue. + maxUnauthenticatedQueueSize: 256 * 1024, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + const closed = waitForClose(attacker) + + // Each frame carries ~64 KB; the 5th frame crosses the 256 KB budget. + for (let i = 0; i < 20; i += 1) { + attacker.send(buildStatelessFrame('queue-bytes-doc', 64 * 1024)) + } + + const code = await closed + t.is(code, 4205, 'connection should be closed with ResetConnection (4205)') +}) + +test('closes an unauthenticated connection that opens too many pending documents', async t => { + const server = await newHocuspocus(t, { + maxPendingDocuments: 3, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + const closed = waitForClose(attacker) + + // Spread a single small frame across many distinct document keys. A per-key + // limit alone would not catch this amplification; a per-connection cap does. + for (let i = 0; i < 25; i += 1) { + attacker.send(buildStatelessFrame(`pending-doc-${i}`, 16)) + } + + const code = await closed + t.is(code, 4205, 'connection should be closed with ResetConnection (4205)') +}) + +test('closes a continuously-sending unauthenticated connection once the pre-auth timeout elapses', async t => { + const server = await newHocuspocus(t, { + timeout: 1000, + // Keep queue limits high so the timeout — not the queue cap — is what fires. + maxUnauthenticatedQueueMessages: 1_000_000, + maxUnauthenticatedQueueSize: 1024 * 1024 * 1024, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + + const closed = waitForClose(attacker, 6000) + + // Keep refreshing activity with tiny frames. The pre-auth deadline must not + // be reset by inbound traffic, so the connection still closes. + const interval = setInterval(() => { + if (attacker.readyState === WebSocket.OPEN) { + attacker.send(buildStatelessFrame('timeout-doc', 8)) + } + }, 200) + t.teardown(() => { + clearInterval(interval) + attacker.close() + }) + + const code = await closed + clearInterval(interval) + t.is(code, 4408, 'connection should be closed with ConnectionTimeout (4408)') +}) + +test('counts in-flight authentications toward the pending-document limit', async t => { + const server = await newHocuspocus(t, { + maxPendingDocuments: 3, + // Never resolves, so every document stays in the "authenticating" state. + // Sending Auth first marks a document as establishing but keeps its hook + // payload alive while onAuthenticate runs — this must still count. + onAuthenticate: () => new Promise(() => {}), + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + const closed = waitForClose(attacker) + + // Send Auth as the FIRST frame for many distinct documents. + for (let i = 0; i < 25; i += 1) { + attacker.send(buildAuthFrame(`auth-first-doc-${i}`, '')) + } + + const code = await closed + t.is(code, 4205, 'connection should be closed with ResetConnection (4205)') +}) + +test('does not close an authenticated connection that has closed all of its documents', async t => { + const server = await newHocuspocus(t, { + timeout: 600, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + // Authenticate a document (no onAuthenticate hook => allowed). + const authenticated = new Promise((resolve) => { + attacker.addEventListener('message', () => resolve(), { once: true }) + }) + attacker.send(buildAuthFrame('soon-empty-doc', '')) + await authenticated + + let closedCode: number | undefined + attacker.addEventListener('close', (event) => { closedCode = event.code }, { once: true }) + + // Keep the socket alive with periodic frames so `lastMessageReceivedAt` stays + // fresh. Halfway through, close the only document, leaving zero established + // document connections. Because the connection already authenticated, the idle + // check must keep using `lastMessageReceivedAt` rather than the original open + // time — so the socket must survive past `timeout` measured from open. + const interval = setInterval(() => { + if (attacker.readyState === WebSocket.OPEN) { + attacker.send(buildStatelessFrame('keepalive-doc', 8)) + } + }, 150) + t.teardown(() => clearInterval(interval)) + + await new Promise((resolve) => setTimeout(resolve, 700)) + attacker.send(buildCloseFrame('soon-empty-doc')) + await new Promise((resolve) => setTimeout(resolve, 1100)) + + clearInterval(interval) + t.is(closedCode, undefined, 'authenticated connection must not be closed by the pre-auth deadline') +}) + +test('does not close an authenticated connection that later exceeds the pre-auth limits', async t => { + const server = await newHocuspocus(t, { + maxUnauthenticatedQueueMessages: 5, + maxUnauthenticatedQueueSize: 64 * 1024, + }) + + const attacker = await openRawSocket(server.server!.webSocketURL) + t.teardown(() => attacker.close()) + + // Authenticate first (no onAuthenticate hook => allowed). + const authenticated = new Promise((resolve) => { + attacker.addEventListener('message', () => resolve(), { once: true }) + }) + attacker.send(buildAuthFrame('authenticated-doc', '')) + await authenticated + + // After auth, large/many frames flow through the established connection and + // are not subject to the pre-auth queue caps. + let closedEarly = false + attacker.addEventListener('close', () => { closedEarly = true }, { once: true }) + for (let i = 0; i < 50; i += 1) { + attacker.send(buildStatelessFrame('authenticated-doc', 64 * 1024)) + } + + await new Promise((resolve) => setTimeout(resolve, 300)) + t.false(closedEarly, 'authenticated connection must not be closed by pre-auth queue limits') +}) diff --git a/tests/server/websocketError.ts b/tests/server/websocketError.ts new file mode 100644 index 0000000..fd3a385 --- /dev/null +++ b/tests/server/websocketError.ts @@ -0,0 +1,38 @@ +import test from 'ava' +import type { onAuthenticatePayload } from '@hocuspocus/server' +import { newHocuspocus, newHocuspocusProvider, newHocuspocusProviderWebsocket } from '../utils/index.ts' + +test('does not crash when malformed message is sent pre-authentication', async t => { + await new Promise(async resolve => { + const server = await newHocuspocus(t, { + async onAuthenticate(data: onAuthenticatePayload) { + return new Promise(async resolve => { + setTimeout(resolve, 2000) + }) + }, + }) + + const socket = newHocuspocusProviderWebsocket(t, server) + + let interval: ReturnType + + const provider = newHocuspocusProvider(t, server, { + websocketProvider: socket, + onClose({ event }) { + t.is(event.code, 4401) + clearInterval(interval) + provider.destroy() + }, + onDestroy() { + t.pass() + resolve(true) + }, + }) + + interval = setInterval(() => { + if (socket.webSocket) { + socket.webSocket.send('ϩ') // eslint-disable-line + } + }, 500) + }) +}) \ No newline at end of file diff --git a/tests/transformer/TiptapTransformer.ts b/tests/transformer/TiptapTransformer.ts new file mode 100644 index 0000000..ce2d63f --- /dev/null +++ b/tests/transformer/TiptapTransformer.ts @@ -0,0 +1,73 @@ +import test from 'ava' +import { TiptapTransformer } from '@hocuspocus/transformer' + +test('transforms JSON to Y.Doc', async t => { + const json = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Example Text', + }, + ], + }, + ], + } + + const ydoc = TiptapTransformer.toYdoc(json, 'content') + + t.is( + ydoc.getXmlFragment('content').toJSON(), + 'Example Text', + ) +}) + +test('writes to the correct Y.Doc field', async t => { + const json = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Example Text', + }, + ], + }, + ], + } + + const ydoc = TiptapTransformer.toYdoc(json, 'mySuperCustomField') + + t.is( + ydoc.getXmlFragment('mySuperCustomField').toJSON(), + 'Example Text', + ) +}) + +test('throws a helpful error when the document is empty', async t => { + const invalidJson = null + + const error = t.throws(() => { + TiptapTransformer.toYdoc(invalidJson, 'content') + }, { instanceOf: Error }) + + t.truthy(error?.message.includes('ProseMirror-compatible JSON')) +}) + +test('throws a helpful error when the document is invalid', async t => { + const invalidJson = { + type: 'invalidType', + content: [], + } + + const error = t.throws(() => { + TiptapTransformer.toYdoc(invalidJson, 'content') + }, { instanceOf: Error }) + + t.truthy(error?.message.includes('Unknown node type: invalidType')) +}) diff --git a/tests/utils/_force-exit.ts b/tests/utils/_force-exit.ts new file mode 100644 index 0000000..1fa4b52 --- /dev/null +++ b/tests/utils/_force-exit.ts @@ -0,0 +1,6 @@ +import process from "node:process"; +import { registerCompletionHandler } from "ava"; + +registerCompletionHandler(() => { + process.exit(); // eslint-disable-line +}); diff --git a/tests/utils/createDirectory.ts b/tests/utils/createDirectory.ts new file mode 100644 index 0000000..6b38328 --- /dev/null +++ b/tests/utils/createDirectory.ts @@ -0,0 +1,9 @@ +import fs from 'fs' + +export const createDirectory = (dir: string) => { + try { + fs.mkdir(dir, () => {}) + } catch { + // + } +} diff --git a/tests/utils/flushRedis.ts b/tests/utils/flushRedis.ts new file mode 100644 index 0000000..3fe0d27 --- /dev/null +++ b/tests/utils/flushRedis.ts @@ -0,0 +1,13 @@ +// @ts-expect-error +import { createClient } from 'redis' +import { redisConnectionSettings } from './redisConnectionSettings.ts' + +export const flushRedis = async () => { + const client = createClient({ + url: `redis://${redisConnectionSettings.host}:${redisConnectionSettings.port}`, + }) + + await client.connect() + + return client.flushDb() +} diff --git a/tests/utils/index.ts b/tests/utils/index.ts new file mode 100644 index 0000000..760c2e3 --- /dev/null +++ b/tests/utils/index.ts @@ -0,0 +1,9 @@ +export * from './createDirectory.ts' +export * from './flushRedis.ts' +export * from './newHocuspocus.ts' +export * from './newHocuspocusProvider.ts' +export * from './newHocuspocusProviderWebsocket.ts' +export * from './randomInteger.ts' +export * from './redisConnectionSettings.ts' +export * from './removeDirectory.ts' +export * from './sleep.ts' diff --git a/tests/utils/newHocuspocus.ts b/tests/utils/newHocuspocus.ts new file mode 100644 index 0000000..062c9a4 --- /dev/null +++ b/tests/utils/newHocuspocus.ts @@ -0,0 +1,35 @@ +import type { ExecutionContext } from "ava"; +import type { ServerConfiguration } from "@hocuspocus/server"; +import { Server } from "@hocuspocus/server"; + +export const newHocuspocus = ( + t: ExecutionContext, + options?: Partial, +) => { + const server = new Server({ + // We don't need the logging in testing. + quiet: true, + // Binding something port 0 will end up on a random free port. + // That's helpful to run tests concurrently. + port: 0, + // Ava 6+ considers process.exit() in workers an error. + // Disable signal handlers to prevent that. + stopOnSignals: false, + // Add or overwrite settings, depending on the test case. + ...options, + }); + + t.teardown(() => { + // Close WebSocket connections and shut down the HTTP server. + // We avoid calling server.destroy() because it triggers lifecycle + // hooks (onDestroy, afterUnloadDocument) which could run assertions + // after the test has finished (Ava 6+ treats this as an error). + // The _force-exit completion handler ensures the process exits + // even if some handles remain open. + server.hocuspocus.closeConnections(); + server.httpServer.closeAllConnections(); + server.httpServer.close(); + }); + + return server.listen(); +}; diff --git a/tests/utils/newHocuspocusProvider.ts b/tests/utils/newHocuspocusProvider.ts new file mode 100644 index 0000000..ab8a7d2 --- /dev/null +++ b/tests/utils/newHocuspocusProvider.ts @@ -0,0 +1,32 @@ +import type { ExecutionContext } from "ava"; +import { + HocuspocusProvider, + type HocuspocusProviderConfiguration, + type HocuspocusProviderWebsocket, + type HocuspocusProviderWebsocketConfiguration, +} from "@hocuspocus/provider"; +import type { Hocuspocus } from "@hocuspocus/server"; +import { newHocuspocusProviderWebsocket } from "./newHocuspocusProviderWebsocket.ts"; + +export const newHocuspocusProvider = ( + t: ExecutionContext, + server: Hocuspocus, + options: Partial = {}, + websocketOptions: Partial = {}, + websocketProvider?: HocuspocusProviderWebsocket, +): HocuspocusProvider => { + const provider = new HocuspocusProvider({ + websocketProvider: + websocketProvider ?? + newHocuspocusProviderWebsocket(t, server, websocketOptions), + // Just use a generic document name for all tests. + name: "hocuspocus-test", + // Add or overwrite settings, depending on the test case. + ...options, + }); + provider.attach(); + + t.teardown(() => provider.destroy()); + + return provider; +}; diff --git a/tests/utils/newHocuspocusProviderWebsocket.ts b/tests/utils/newHocuspocusProviderWebsocket.ts new file mode 100644 index 0000000..0116108 --- /dev/null +++ b/tests/utils/newHocuspocusProviderWebsocket.ts @@ -0,0 +1,22 @@ +import type { ExecutionContext } from "ava"; +import type { HocuspocusProviderWebsocketConfiguration } from "@hocuspocus/provider"; +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import type { Hocuspocus } from "@hocuspocus/server"; + +export const newHocuspocusProviderWebsocket = ( + t: ExecutionContext, + hocuspocus: Hocuspocus, + options: Partial> = {}, +) => { + const ws = new HocuspocusProviderWebsocket({ + // We don't need which port the server is running on, but + // we can get the URL from the passed server instance. + url: hocuspocus.server!.webSocketURL, + // Node.js 22+ has native WebSocket support + ...options, + }); + + t.teardown(() => ws.destroy()); + + return ws; +}; diff --git a/tests/utils/randomInteger.ts b/tests/utils/randomInteger.ts new file mode 100644 index 0000000..31a9854 --- /dev/null +++ b/tests/utils/randomInteger.ts @@ -0,0 +1,3 @@ +export const randomInteger = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1) + min) +} diff --git a/tests/utils/redisConnectionSettings.ts b/tests/utils/redisConnectionSettings.ts new file mode 100644 index 0000000..33dcc61 --- /dev/null +++ b/tests/utils/redisConnectionSettings.ts @@ -0,0 +1,4 @@ +export const redisConnectionSettings = { + host: process.env.REDIS_HOST || '127.0.0.1', + port: Number.parseInt(process.env.REDIS_PORT || '', 10) || 6379, +} diff --git a/tests/utils/removeDirectory.ts b/tests/utils/removeDirectory.ts new file mode 100644 index 0000000..025f574 --- /dev/null +++ b/tests/utils/removeDirectory.ts @@ -0,0 +1,27 @@ +import fs from 'fs' +import path from 'path' + +export const removeDirectory = (dir: string) => { + try { + const list = fs.readdirSync(dir) + + for (let i = 0; i < list.length; i += 1) { + const filename = path.join(dir, list[i]) + const stat = fs.statSync(filename) + + if (filename === '.' || filename === '..') { + // pass these files + } else if (stat.isDirectory()) { + // rmdir recursively + removeDirectory(filename) + } else { + // rm fiilename + fs.unlinkSync(filename) + } + } + + fs.rmdirSync(dir) + } catch { + // + } +} diff --git a/tests/utils/retryableAssertion.ts b/tests/utils/retryableAssertion.ts new file mode 100644 index 0000000..c56a6b7 --- /dev/null +++ b/tests/utils/retryableAssertion.ts @@ -0,0 +1,18 @@ +import type { ExecutionContext } from 'ava' +import { sleep } from './sleep.ts' + +/* eslint-disable no-await-in-loop */ +export const retryableAssertion = async (t: ExecutionContext, recoverableTry: (tt: ExecutionContext) => void) => { + // eslint-disable-next-line no-constant-condition + while (true) { + const lastTry = await t.try(recoverableTry) + + if (lastTry.passed) { + lastTry.commit() + break + } + lastTry.discard() + + await sleep(100) + } +} diff --git a/tests/utils/sleep.ts b/tests/utils/sleep.ts new file mode 100644 index 0000000..81f633c --- /dev/null +++ b/tests/utils/sleep.ts @@ -0,0 +1,5 @@ +export const sleep = (time: number) => { + return new Promise(async resolve => { + setTimeout(resolve, time) + }) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d788d1d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "esnext", + "strict": true, + "jsx": "react", + "importHelpers": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "experimentalDecorators": true, + "sourceMap": true, + "baseUrl": ".", + "rootDir": ".", + "allowJs": false, + "checkJs": false, + "paths": { + "@hocuspocus/*": ["packages/*/src/index.ts"] + }, + "lib": ["esnext", "dom"], + "skipLibCheck": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["**/node_modules", "**/dist"] +}