diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 440bc549..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: miguelgrinberg -patreon: miguelgrinberg -custom: https://paypal.me/miguelgrinberg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..d73d5053 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**IMPORTANT**: If you have a question, or you are not sure if you have found a bug in this package, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a discussion to troubleshoot your issue. + +**Describe the bug** +A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. + +**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 00000000..3657c972 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: GitHub Discussions + url: https://github.com/miguelgrinberg/python-socketio/discussions + about: Ask questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..968c279f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Logs** +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..9d2f4759 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: build +on: [push, pull_request, workflow_dispatch] +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox -eflake8 + - run: tox -edocs + tests: + name: tests + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] + exclude: + # pypy3 currently fails to run on Windows + - os: windows-latest + python: pypy-3.10 + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: python -m pip install --upgrade pip wheel + - run: pip install tox tox-gh-actions + - run: tox + - uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index ad85d4ed..01d8b9ad 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ venv* tags htmlcov *.swp + +node_modules diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..4358e806 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 187660b0..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,3 +0,0 @@ -python: - version: 3.6 - setup_py_install: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 20f6a6e8..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -dist: xenial -language: python -matrix: - include: - - python: 3.7 - env: TOXENV=flake8 - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: 3.7 - env: TOXENV=py37 - - python: 3.8 - env: TOXENV=py38 - - python: pypy3 - env: TOXENV=pypy3 - - python: 3.7 - env: TOXENV=docs -install: - - pip install tox -script: - - tox diff --git a/CHANGES.md b/CHANGES.md index e6034d10..fd061624 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,298 @@ # python-socketio change log +**Release 5.13.0** - 2025-04-12 + +- Eliminate race conditions on disconnect [#1441](https://github.com/miguelgrinberg/python-socketio/issues/1441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/288ebb189d799a05bbc5979a834433034ea2939f)) +- Preserve exception context in `Client.connect` and `AsyncClient.connect` [#1450](https://github.com/miguelgrinberg/python-socketio/issues/1450) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5c93c59648358862514f317838f61498a101ba54)) (thanks **Tim Van Baak**!) +- Allow custom client subclasses to be used in SimpleClient and AsyncSimpleClient [#1432](https://github.com/miguelgrinberg/python-socketio/issues/1432) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7605630bb236b4baf98574ca2a8f0cdba2696ef4)) +- Add support for Redis Sentinel URLs in `RedisManager` and `AsyncRedisManager` [#1448](https://github.com/miguelgrinberg/python-socketio/issues/1448) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6a52e8b50274a7524fadcd2633eb819811a63734)) +- Remove incorrect reference to an `asyncio` installation extra in documentation [#1449](https://github.com/miguelgrinberg/python-socketio/issues/1449) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/537630b983245cc137f609c3e6247d6d68ebdea5)) + +**Release 5.12.1** - 2024-12-29 + +- Fix admin instrumentation support of disconnect reasons [#1423](https://github.com/miguelgrinberg/python-socketio/issues/1423) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b75fd31625cfea0d8c67d776070e4f8de99c1e45)) +- Stop using deprecated datetime functions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8fe012abbb350107b742ab2cf9aa44d328bc23e9)) +- Enable admin instrumentation by default in WSGI and ASGI examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/269332da8041df115e3a1e2ca04808c3179a72e1)) +- Fixed broken gevent URL in documentation [#1427](https://github.com/miguelgrinberg/python-socketio/issues/1427) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8964dab9d545333646fafad9aae0becd761a1045)) (thanks **Carlos Guerrero**!) + +**Release 5.12.0** - 2024-12-18 + +- Added a `reason` argument to the disconnect handler [#1422](https://github.com/miguelgrinberg/python-socketio/issues/1422) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd8555da8523d1a73432685a00eb5acb4d2261f5)) +- Prevented starting multiple tasks for reconnection [#1369](https://github.com/miguelgrinberg/python-socketio/issues/1369) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b6ee33e56cf2679664c1b894bf7e5d33a30976db)) (thanks **humayunsr**!) +- Fixed `AsyncClient::wait()` unexpected return after success reconnect [#1407](https://github.com/miguelgrinberg/python-socketio/issues/1407) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/78d1124c50cff149051fb89bf6f08000bf184da5)) (thanks **Arseny**!) +- Removed old constructs from old and unsupported Python versions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db642bb2bd9794eeceddd54abc47665c69e85406)) +- Removed dependency on unittest.TestCase base class in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/abf336e108b01f44afb473eb86c1dece6360195c)) +- Adopted pyenv-asyncio for async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0b5c4638e5e4bff06fcf46476d218ae5ad4ada14)) +- Adopted unittest.mock.AsyncMock in async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8f0e66c1cd1cd63dcef703576cc9cb9c99104df7)) +- Fix typo with `AsyncClient.connect` example [#1403](https://github.com/miguelgrinberg/python-socketio/issues/1403) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72d37ea79f4cf6076591782e4781fd4868a7e0d6)) (thanks **Peter Bierma**!) +- Documentation typo [#1421](https://github.com/miguelgrinberg/python-socketio/issues/1421) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf5a05ae9bf94b2fbd1367a04884ef5a39cd2671)) (thanks **Masen Furer**!) +- Renamed flask-socketio references to python-socketio in documentation [#1377](https://github.com/miguelgrinberg/python-socketio/issues/1377) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5f83cd0f7b2911705eaf1a8cb9060afbee6eb456)) +- Added Python 3.13 CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/42da5d2f5426e812fd37d4cabcb9277810cae9c1)) + +**Release 5.11.4** - 2024-09-02 + +- Prevent crash when client sends empty event ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b901de0077322eedb3509318ccba939f5f0bf10)) +- Add missing `to` argument in manager's `emit()` method [#1374](https://github.com/miguelgrinberg/python-socketio/issues/1374) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f1476041e5bb0857a99024c9a38203edfc974bdf)) (thanks **Pavieł Michalkievič**!) +- Reorganization of server documentation [#1350](https://github.com/miguelgrinberg/python-socketio/issues/1350) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3a618c67ef60736d5619b9ac52c47d96a6acf3c3)) +- Update documentation note about Sanic issues [#1365](https://github.com/miguelgrinberg/python-socketio/issues/1365) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/287d6ed551090eed822f924bb548ba93923dd4d1)) + +**Release 5.11.3** - 2024-06-19 + +- New `shutdown()` method added to the client [#1333](https://github.com/miguelgrinberg/python-socketio/issues/1333) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/811e044a46b7d6e4d94bf870e59d0cd8187850d3)) +- Ignore catch-all namespace in client connections [#1351](https://github.com/miguelgrinberg/python-socketio/issues/1351) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/469b7c0dd51eea97979f784d0c94359ad18a96ac)) +- Accept 0 as a callback id [#1329](https://github.com/miguelgrinberg/python-socketio/issues/1329) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e59351969241c3d7f44560027cf1e44206e17d6c)) (thanks **Ruslan Bel'kov**!) +- Minor updates to the server and client documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e78ecbc343d9c7252a6449029263b4a5cb967c8)) +- Remove outdated information in intro section of documentation [#1337](https://github.com/miguelgrinberg/python-socketio/issues/1337) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/82ceaf7a23c51ed1911e550aaf71d6449584eaa0)) +- Fixed typos in server documentation [#1331](https://github.com/miguelgrinberg/python-socketio/issues/1331) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/547449bc70248b1a2c68e362a026d42db083222c)) (thanks **John Sigg**!) + +**Release 5.11.2** - 2024-03-24 + +- Improved routing to catch-all namespace handlers [#1316](https://github.com/miguelgrinberg/python-socketio/issues/1316) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd39b8f2156f600ea6de558af03a6be205d6e892)) (thanks **asuka**!) +- Option to disable routing in ASGIApp ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7cc84bd13dfa0a86b0326446fb6954949ef948c0)) + +**Release 5.11.1** - 2024-02-05 + +- Connection retry option in the client [#1306](https://github.com/miguelgrinberg/python-socketio/issues/1306) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b4f36148d816b2fbf2a81237dff70b18d909aebf)) +- use Socket.IO sid in transport[#1299](https://github.com/miguelgrinberg/python-socketio/issues/1299) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0e1f23229dc5a478df474e5bb6e5c91e5e30aa04)) +- Add support for Python 3.12 and drop 3.7 [#1297](https://github.com/miguelgrinberg/python-socketio/issues/1297) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/417785293f6c1351be6ea10b6b9f57abff03fdfc)) (thanks **Hugo van Kemenade**!) + +**Release 5.11.0** - 2024-01-06 + +- Support catch-all namespaces [#1288](https://github.com/miguelgrinberg/python-socketio/issues/1288) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/801241378ec6cbbaef6490399220b00473f66d34)) (thanks **mooomooo**!) +- Prevent pubsub managers from ever crashing [#1262](https://github.com/miguelgrinberg/python-socketio/issues/1262) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/12134bd5c50851a1c1cd9ac319beeb7b12a9398b)) +- Hold references to background tasks to avoid garbage collection [#1191](https://github.com/miguelgrinberg/python-socketio/issues/1191) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f07824347190026e3dc40c66cffbb5604d09799)) +- Clearer documentation for the `max_http_buffer_size` argument [#1272](https://github.com/miguelgrinberg/python-socketio/issues/1272) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1f488b03b669ee8b4435e342ff00d8b88f43d5fc)) +- Improved catch-all handler documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0a54ec6ae46848b336849d6c198c11a6e1dcf51f)) +- Documentation typos ([commit](https://github.com/miguelgrinberg/python-socketio/commit/29c794333ec284bd8f968db68d24738ef2b43923)) +- Remove documentation references to gevent-websocket, which is not needed anymore ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0aa8683a3b13f5175fea70e52f889297d605b2bc)) +- Remove documentation reference to outdated aioredis package [#1268](https://github.com/miguelgrinberg/python-socketio/issues/1268) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/344e5c1d6b3927464cf31069cea198569c6540b3)) +- Update requirements for Django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f72139b89e4de9eb38aa90d62ca0fa205db196d9)) +- Unit test fixes for the new simple clients [#1265](https://github.com/miguelgrinberg/python-socketio/issues/1265) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/66b9586a65564d82ae3589c65e541a0e593d90d3)) + +**Release 5.10.0** - 2023-10-15 + +- New `SimpleClient` and `AsyncSimpleClient` classes [#1237](https://github.com/miguelgrinberg/python-socketio/issues/1237) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/55d6310eb3e194b1e67e40942a953bc4413b98b7)) ([commit #2](https://github.com/miguelgrinberg/python-socketio/commit/699ee9c47adcb44f1a8150d8c92a9555d07f7b5b)) +- Reporting to Socket.IO Admin UI (https://admin.socket.io) [#1164](https://github.com/miguelgrinberg/python-socketio/issues/1164) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4bf48776ca08acba255bf660f5ca61810202826a)) +- Support entering and leaving rooms through pubsub client managers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d222f4c3deb7a12f3a8c584777dbfc18448a206d)) +- Async versions of enter_room and leave_room should be coroutines (**breaking change**) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8e3460c5fcc351ca0cff83c690eedcde0e8a6e7e)) +- Add a `shutdown()` function for the server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c419fc5481846ab026ca847234f5ebe5420e510a)) +- Message queue optimizations [#1240](https://github.com/miguelgrinberg/python-socketio/issues/1240) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc6e4f516f38d1125cb44ccae11b120ffa8fe7f3)) +- Remove unneeded arguments from super() ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8da3c617a66877ec0fc5ab25256ead41ae611351)) +- Internal code restructure (no functional changes) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/ef0f88f6cf5687dddfd0e3be9a90af2865d5871f)) ([commit #2](https://github.com/miguelgrinberg/python-socketio/commit/58b57068ab770d099bdd54d4eb8d22ce8a4f6751)) +- Add FastAPI and Litestar server examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2e1f0262ad9a037858b04b9dcec7509ee5008f8c)) +- Update Socket.IO JavaScript client to version 4.7.2 in all examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3f78af283171d23b7f1dbfd055e21afe00877399)) +- Update `ping_timeout` documented default to accord with current Engine.IO behavior [#1255](https://github.com/miguelgrinberg/python-socketio/issues/1255) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d40b3a33fff5c6b896559fc534ccd611ab9cf1f4)) (thanks **[object Object]**!) +- Refactor common testing helpers into a separate module ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7208ec09e17466758cebb0732801ec84b25f1299)) +- Migrate Python package metadata to pyproject.toml ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6bdc498d423ca793b3db2d158fba4a461610de6a)) + +**Release 5.9.0** - 2023-09-03 + +- Optimized performance and memory usage for broadcasts [#1233](https://github.com/miguelgrinberg/python-socketio/issues/1233) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf11ad36aebeebbc0b7a74d88d4b9a8fad113456)) +- Improved documentation on horizontal scaling [#1099](https://github.com/miguelgrinberg/python-socketio/issues/1099) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9d02247bc5914798a965cf423e978747a5535e33)) +- Corrected user session documentation example [#1170](https://github.com/miguelgrinberg/python-socketio/issues/1170) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72dfdc6c8aff7f4f61c409e771df6a19299ad36f)) +- Improved grammar in documentation [#1175](https://github.com/miguelgrinberg/python-socketio/issues/1175) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c66d7a9f1ade0f728840b8ee9079599dd6a3de3f)) (thanks **zykron1**!) +- Fix docstring typo: client/server mixup [#1163](https://github.com/miguelgrinberg/python-socketio/issues/1163) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/598dd7e258c0c1d050456bbad2cd867dfca3df0f)) (thanks **Sasja**!) +- Fix typos in the documentation [#1230](https://github.com/miguelgrinberg/python-socketio/issues/1230) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b7ed682e06d7c8a758a2f89391ddc941f70ee98)) (thanks **Alex Kuhrt**!) +- Upgrade dependencies in Django server example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/00b31663c29968bd50282d22fca9b95c09e4becf)) +- Update reference JavaScript examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6df96fb1c598c00ddf8f8995cbc095f0383d38ef)) +- Upgrade to pypy-3.9 in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f49d65a0c3ff55f92a817994898c4182efa5ba69)) + +**Release 5.8.0** - 2023-03-16 + +- Made kombu client manager more robust and efficient ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8293dc3f8fa90f3d92192a702b28c23d8c516110)) +- Made aio_pika client manager more robust and efficient [#1142](https://github.com/miguelgrinberg/python-socketio/issues/1142) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cd7f781c022dd1d1ec3c6695a0fd6ab3ce864fd5)) +- Correctly handle emits to multiple rooms in the async server [#1081](https://github.com/miguelgrinberg/python-socketio/issues/1081) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/232cef1f86ff19878190c44caf991e017c8480a4)) +- Expose the `ignore_queue` option in namespaces [#1103](https://github.com/miguelgrinberg/python-socketio/issues/1103) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1cadada02dd7dc1eb96f45e88cbec67e1a393db3)) +- Do not automatically import zmq ([commit](https://github.com/miguelgrinberg/python-socketio/commit/de4d5b51e5fc8ba0d0f904851f23f8cced16d7f6)) +- TLS/SSL client documentation [#1040](https://github.com/miguelgrinberg/python-socketio/issues/1040) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/54aecfda917ec100e0b5e2c0e955ef719e0eb645)) +- Removed incorrect reference to multiple callback invocations in documentation [#1152](https://github.com/miguelgrinberg/python-socketio/issues/1152) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c4117fd6514374042127ab05f1f2d8d221fcf60d)) +- Fix documentation typo [#1155](https://github.com/miguelgrinberg/python-socketio/issues/1155) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/270eb372cc83778a897d32e95453eb385328d9de)) (thanks **Onwuka Gideon**!) +- Fix documentation typos ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c747ab67b3e5c9f31db540a13c3da1b7784617c)) +- Fix documentation typo in asyncio_server.py [#1150](https://github.com/miguelgrinberg/python-socketio/issues/1150) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b2cc86cfb2502691d70447b0002179602a798e77)) (thanks **riz-j**!) +- Fix documentationi type [#1091](https://github.com/miguelgrinberg/python-socketio/issues/1091) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/55db7458900a179a9363294cc4fc91eb9c775f54)) (thanks **mostlycryptic**!) +- Add Python 3.11 to builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/60fe63b098af8c035891ae62a4336538ea419184)) + +**Release 5.7.2** - 2022-10-17 + +- Fixed disconnect implementation when using a message queue [#1002](https://github.com/miguelgrinberg/python-socketio/issues/1002) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f56ef6f0401b273107fc483c4ae1b5512209ac48)) +- Fixed remote async disconnects via message queue [#1003](https://github.com/miguelgrinberg/python-socketio/issues/1003) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/104d6569a0480ed0adb04e7d41f156762f9ebe9b)) +- Support optional payloads in msgpack implementation [#981](https://github.com/miguelgrinberg/python-socketio/issues/981) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ce1afd79e69e35b81a6f0d02fbd7ae04af59f9d6)) (thanks **Cromfel**!) +- Recommend ASGI integration for Sanic in Documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2c3e360ae8b151bc0bfedbde50248cd0dc8d1ff9)) + +**Release 5.7.1** - 2022-07-15 + +- Add `namespaces` argument to `Server` and `AsyncServer` [#822](https://github.com/miguelgrinberg/python-socketio/issues/822) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/efe87d867a205493654107d381bdb8b619b8ab2d)) +- Add missing `await` in asyncio server [#952](https://github.com/miguelgrinberg/python-socketio/issues/952) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d4e69fb7ceecdb98584f36e085a186eb4da23b07)) (thanks **sjrodahl**!) + +**Release 5.7.0** - 2022-07-04 + +- Server refuses connections on unknown namespaces [#822](https://github.com/miguelgrinberg/python-socketio/issues/822) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/44715012dc0d578b067a9a389c8aef2ce39f65c1)) +- Do not send ACK packet for unknown events [#824](https://github.com/miguelgrinberg/python-socketio/issues/824) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/268fe12ffa68f7af8881c75695c287c09490cef9)) +- Fix Python 3.11 deprecation warning [#941](https://github.com/miguelgrinberg/python-socketio/issues/941) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4b697815c3da4574fca8b759d21a4e0800dafc50)) (thanks **Jérôme Boulmier**!) +- Correct handling of RedisError exception [#919](https://github.com/miguelgrinberg/python-socketio/issues/919) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98318fbdde2c4dcfba15d1b0aaf266b599e81e0c)) +- Update Django example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc7ac74c1d2c97544056541736d644060837a080)) +- Documentation fix for async client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5b9134617759a1b64adb2f9aba0974c732576cc4)) +- Update documentation of asyncio server ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98f3cb4664ff10c0bb17826b11564644bed99fd6)) +- Fix documentation typo [#948](https://github.com/miguelgrinberg/python-socketio/issues/948) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f888446b330146484325b568cdc2303c9b35c095)) (thanks **mostlycryptic**!) + +**Release 5.6.0** - 2022-04-24 + +- Catch and log errors in pubsub listening thread [#889](https://github.com/miguelgrinberg/python-socketio/issues/889) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f2ae136dcd724d56353b783092c448d6e638635f)) +- Use new asyncio support in redis package [#911](https://github.com/miguelgrinberg/python-socketio/issues/911) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0e7691b77605de00dedcbf87e415bb79fcd7f2aa)) +- Add support for aiopiko version 7 and higher [#897](https://github.com/miguelgrinberg/python-socketio/issues/897) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b4b55e1fb5e9d837b3301b5b873d9a7fe2a12023)) (thanks **Dmitriy**!) +- Fixed documentation typo [#910](https://github.com/miguelgrinberg/python-socketio/issues/910) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/16243e7c5f95f73f1f17b6febbf23c6483c17c62)) (thanks **Omar Costa Hamido**!) +- Fix aiohttp example's background task [#881](https://github.com/miguelgrinberg/python-socketio/issues/881) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fb9648575ef5bd529579f854f141df52d9e000ed)) +- Bump sanic from 0.8 to 20.12.6 in /examples/server/sanic [#875](https://github.com/miguelgrinberg/python-socketio/issues/875) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/342f2e3fe9a3f973a491326b19fa4f96705b9b7e)) (thanks **dependabot[bot]**!) +- Add application name to Sanic example [#892](https://github.com/miguelgrinberg/python-socketio/issues/892) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4d5e36d36f865bf0527cfeb486a4e7f917b28717)) (thanks **Florian Metzger-Noel**!) + +**Release 5.5.2** - 2022-02-15 + +- Connect with an empty auth object instead of `None` [#861](https://github.com/miguelgrinberg/python-socketio/issues/861) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1fb7a76575426dd58a5e9c0e01646302ccc96188)) +- Fix indentation in the "Rooms" docs example. [#872](https://github.com/miguelgrinberg/python-socketio/issues/872) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3336181f9ce5fe737d675f8343f18a885c651ebd)) (thanks **Ezio Melotti**!) +- Remove 3.6 and pypy-3.6 builds, add 3.10 and pypy-3.8 ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ed5679a7cb01963b44a5004e1236b7e8b485aa0b)) + +**Release 5.5.1** - 2022-01-11 + +- Support multiple Kafka servers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4ee3649514b98c50cc0bf70d3f269389da52772d)) (thanks **sparkingdark**!) +- Include example code in flake8 pass ([commit](https://github.com/miguelgrinberg/python-socketio/commit/273a4b0439c84560d403662d8eba4122c28ba0d8)) + +**Release 5.5.0** - 2021-11-14 + +- Option to disable the SIGINT handler in the client [#792](https://github.com/miguelgrinberg/python-socketio/issues/792) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ea84b9b1c714b02eaf1081f4e37fd130a3159d8c)) +- Do not invoke reserved events on a catch-all handler [#814](https://github.com/miguelgrinberg/python-socketio/issues/814) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/34f34e53d650dde605f5f4a98d7a70936524a1b8)) +- Use correct binary packet types in the msgpack packet encoder [#811](https://github.com/miguelgrinberg/python-socketio/issues/811) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/60735dd4c2fc87ed863d7dbf7de361500d963dd3)) +- Add missing `call()` method to namespace classes [#800](https://github.com/miguelgrinberg/python-socketio/issues/800) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/32db48d12ceb44d7a02fd9f05047b47c7ed3f4a5)) +- Add missing `to` argument to namespace `emit()` and `send()` methods [#810](https://github.com/miguelgrinberg/python-socketio/issues/810) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ed08a01e65635160923f3d6d5755df74d53274e1)) +- Configure Redis pubsub to skip subscription messages ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e8fff07b367929794e5e30cecbf252b72d307c16)) +- Migrate async Redis client manager to aioredis 2 [#771](https://github.com/miguelgrinberg/python-socketio/issues/771) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f245191d86722244a2d3d0529d9f5ff15dfd817a)) (thanks **Sam Mosleh**!) +- Update Python supported versions in docs ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a54152f2466bad4869d9cfdad6be3a5547e0b6bc)) +- Document how to get the connection state in the client [#799](https://github.com/miguelgrinberg/python-socketio/issues/799) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/47c5f45c765ae207f58ba2675f91eaf8c79f8500)) +- Improved documentation of `start_background_task()` function ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4f5bf1e9898154aa1a9896a7016ba22bfb73cdf2)) +- Improved documentation of `call()` method [#813](https://github.com/miguelgrinberg/python-socketio/issues/813) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c2a6ac86972bf94acafe687d2e86bdf65119960)) +- Fixed intermittent test failures [#572](https://github.com/miguelgrinberg/python-socketio/issues/572) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db0565ada6c8891be3230bcc415e5465bd409c09)) + +**Release 5.4.1** - 2021-10-14 + +- Catch-all event handlers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/28569d48ad74d5414a0d2a8f69d7540dbdddf066)) +- Implement disconnect method for external processes [#684](https://github.com/miguelgrinberg/python-socketio/issues/684) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a830c9f7887df715227f4284f30e8d62680e58ce)) + +**Release 5.4.0** - 2021-08-02 + +- Support msgpack and custom packet serializers [#749](https://github.com/miguelgrinberg/python-socketio/issues/749) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5159e84c49daaf2da0579bfc6ee954a9c738a076)) +- Return error packet if client connects to an already connected namespace ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cb1b8ec74bee3b5247200a6fc6e3f6aab3a3f941)) +- Handle CancelledError in async pubsub managers [#750](https://github.com/miguelgrinberg/python-socketio/issues/750) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a813bde0626c16fe693db74cbc2ea7c331a177d3)) +- More robust handling of emit's "to" argument [#689](https://github.com/miguelgrinberg/python-socketio/issues/689) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f0d8bbd8c4de43fe26e0f2edcd05aef3c8c71f9)) +- Remove executable permissions from setup.py, which has no shebang [#748](https://github.com/miguelgrinberg/python-socketio/issues/748) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/fb3ac958dfa2d3b04f8ec1b0221c3f4734c9f756)) (thanks **Ben Beasley**!) +- Improved project structure ([commit](https://github.com/miguelgrinberg/python-socketio/commit/98c7ac23f231b64cc8b8c51104b792d0cd5cf361)) + +**Release 5.3.0** - 2021-05-15 + +- Document WebSocket support for threading mode ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2f085b3338acc56a5c4625783d50ad53f5ad0c1a)) +- Allow functions to be used for URL, headers and auth data in client connection [#588](https://github.com/miguelgrinberg/python-socketio/issues/588) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7d2e7f7eb3eb34860c2b28df1807a932ed632b54)) +- Emit events to multiple rooms [#605](https://github.com/miguelgrinberg/python-socketio/issues/605) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2538df8bcf0e44881a7653cc684f985252c7fce0)) +- More descriptive error when joining a room on a bad namespace [#650](https://github.com/miguelgrinberg/python-socketio/issues/650) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/66068d9e968942343c840609e6e062162256111b)) +- Document the use of arguments in the `connect_error` handler [#554](https://github.com/miguelgrinberg/python-socketio/issues/554) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d5308504d304b1bd26398309d31d0ce5fbd76e74)) +- Document that callbacks cannot be used in external processes [#1533](https://github.com/miguelgrinberg/Flask-SocketIO/issues/1533) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/703843b42b8fb7fb6a9d0152610e714e9c6fb75e)) +- Improve `start_background_task()` example in the documentation [#647](https://github.com/miguelgrinberg/python-socketio/issues/647) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/ef4ae900c5245439921e706fa46008fe5ca102d6)) +- Added Open Collective funding option ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6572ad683ac87981493f69307d5e01cb15a0dacb)) +- Remove Python 2 from PyPI classifiers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a37ab00b344e035adbdc532f2760c223db118c0f)) + +**Release 5.2.1** - 2021-04-18 + +- Fixed incorrect handling of dashes inside the JSON payload of a packet [#675](https://github.com/miguelgrinberg/python-socketio/issues/675) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f4e101079fdc49c673ab9433aca8346480cc9e4f)) + +**Release 5.2.0** - 2021-04-17 + +- Pass custom authentication data with client connection [#661](https://github.com/miguelgrinberg/python-socketio/issues/661) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a07eedf54e535843401cd8b280c1bb24de3dfd27)) +- Configure the JSON decoder for safer parsing ([commit](https://github.com/miguelgrinberg/python-socketio/commit/81b0b849bd7329c7fef2f6a9491aeae279d7b6e5)) (thanks **Onno Kortmann**!) +- Made parsing of id field of Socket.IO packet faster and more robust ([commit](https://github.com/miguelgrinberg/python-socketio/commit/09cb411776b9035343d7349650bc4b84715f00fd)) (thanks **Onno Kortmann**!) +- Correct use of a trailing comma in Socket.IO packets with no id or data [#671](https://github.com/miguelgrinberg/python-socketio/issues/671) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8d1aeb2e401713758a0a2576d0d1ea8eaf14896e)) +- Updated Socket.IO JavaScript client versions in documentation example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9ff8bf354110e860c73f3298278c8be6ba44cb64)) + +**Release 5.1.0** - 2021-03-10 + +- Added `wait` argument to client's connect method [#634](https://github.com/miguelgrinberg/python-socketio/issues/634) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4da6d74f56a58e68b0aef08212347097dd73cda9)) +- Invoke the disconnect handler when the client initiates a disconnection [#594](https://github.com/miguelgrinberg/python-socketio/issues/594) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3349b024d59a78a6a7282257941b4623af72d7c9)) +- Pass auth information sent by client to the connect handler ([commit](https://github.com/miguelgrinberg/python-socketio/commit/11b6f1a08d4840cc2f20a644bd9db7d5d95496bf)) +- Catch all possible Redis errors [#635](https://github.com/miguelgrinberg/python-socketio/issues/635) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6f812ef8e4b86db8d15fcc9a4d79b721fe7ca068)) +- Reset message queue sleep timer upon reconnect ([commit](https://github.com/miguelgrinberg/python-socketio/commit/54180987cd429c6d48c6fdad5c97b1ca894d2b09)) (thanks **Ed Serzo**!) +- Fixed bad event object used by asyncio client reconnect logic [#622](https://github.com/miguelgrinberg/python-socketio/issues/622) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bff76c432c12e678cc1043d8b046bd4a2fe22c28)) +- Adding missing example of async client implementation to documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f341abe88ec19b20cd115dd246dd4bc2b3ad61fe)) (thanks **manuel**!) +- Add scrolling to documentation sidebar [#610](https://github.com/miguelgrinberg/python-socketio/issues/610) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/eabcc4679bc283acdb9f87022ef1e0e82c48497e)) (thanks **Mohammed Abdul Raheem**!) +- Typo fix in documentation [#602](https://github.com/miguelgrinberg/python-socketio/issues/602) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b418af4c53619305c0b91a040e40fc7f3b907a74)) (thanks **Tim Gates**!) + +**Release 5.0.4** - 2020-12-25 + +- Include error message and arguments in CONNECT_ERROR packet [#590](https://github.com/miguelgrinberg/python-socketio/issues/590) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/314971c8a0ba327acd12b0ecfef84f0a5dd63bed)) +- Fix typos in the documentation [#599](https://github.com/miguelgrinberg/python-socketio/issues/599) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c809774c3ec0921306e7274987076b3ce51a4e95)) (thanks **Arpit Jain**!) +- Updated connection options in the documentation [#597](https://github.com/miguelgrinberg/python-socketio/issues/597) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/251fee1763df168fd070b89d83f24698ba0b3bb8)) + +**Release 5.0.3** - 2020-12-14 + +- Correct handling of user session [#585](https://github.com/miguelgrinberg/python-socketio/issues/585) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a61d59c02aa08483f86bf45accf6620b69b06a41)) +- Performace tuning ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bcdf9bb009ef32dac156ea518c1b113c0773877c)) +- Updated ASGI examples to latest release ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8c5b762d724c0cfc45f982ffc3303202985ab812)) +- Updated Django example to latest release ([commit](https://github.com/miguelgrinberg/python-socketio/commit/94afcf643a66d85b64d76f4c219afe8881e8dbe6)) + +**Release 5.0.2** - 2020-12-12 + +- Return `environ`` dictionary for a client ([commit](https://github.com/miguelgrinberg/python-socketio/commit/2e71c22c6db41b82d863b3189c122ea9f7916bf0)) + +**Release 5.0.1** - 2020-12-08 + +- Fix Engine.IO dependency version ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d2bb2b12e536036e34d94a1ad87e0d48a1a504a8)) +- Conversion from Socket.IO sid to Engine.IO sid ([commit](https://github.com/miguelgrinberg/python-socketio/commit/805b33fa7d2e38be4ada60e382c989433fd5af03)) + +**Release 5.0.0** - 2020-12-07 + +- Update to match the JavaScript Socket.IO 3.x releases (Socket.IO v5 protocol revision) + - v5 protocol: do not connect the default namespace unless requested explicitly ([commit](https://github.com/miguelgrinberg/python-socketio/commit/49822e6919d3de9d52f6dde32c7f04ad62d73990)) + - v5 protocol: handle per-namespace sids in base manager ([commit](https://github.com/miguelgrinberg/python-socketio/commit/308b0c8eeb71e1fead35d19088a3291a15ccd50a)) + - v5 protocol: rename ERROR packet to CONNECT_ERROR ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4940fc1e1e2ddc86c28cd3b626dad75d6845243f)) + - v5 protocol: use Engine.IO 4.x +- Remove unnecessary binary argument ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9270a5bcf85785935520ef816d314a5e197ed227)) +- Remove dependency on the six package ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f6eeedb767614fb68b41927c8fd620c95cafcc6c)) +- Added version compatibility chart to README ([commit](https://github.com/miguelgrinberg/python-socketio/commit/342ca0bb9da8b9ea6c63aa3bd05a37903416d301)) + +**Release 4.6.1** - 2020-11-28 + +- Added troubleshooting section to the documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/48906071307d79df356379fe6e05a73b0c65d9d4)) +- Document the use of tuples when emitting ([commit](https://github.com/miguelgrinberg/python-socketio/commit/3ac3437af781d54a343b4430d1f3546c580677e3)) +- Handle the case of not having a previous signal handler [#518](https://github.com/miguelgrinberg/python-socketio/issues/518) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8107216848672792e420b3254b5217e51bcd4b32)) (thanks **David Brooks**!) +- Simplify asserts in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a4f9992d34a49350a853d9f1a0c3d63034785ab3)) +- Fixed route path for tornado server [#494](https://github.com/miguelgrinberg/python-socketio/issues/494) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/4385057c34bb468b9741edcd918e926a17c9af7c)) (thanks **someApprentice**!) +- Use pytest as test runner ([commit](https://github.com/miguelgrinberg/python-socketio/commit/280d132d284699cc1ae641b1e41403ceab6c66f5)) +- Move builds to GitHub actions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/104d5dd97b207cf59f47f1c72b408ed4b1e6f770)) +- Fixed typo in documentation [#524](https://github.com/miguelgrinberg/python-socketio/issues/524) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1fbf95940cfceca13a2033fb3d455810a3b34e83)) (thanks **Fover**!) + +**Release 4.6.0** - 2020-05-23 + +- Improved handling of rejected connections [#391](https://github.com/miguelgrinberg/python-socketio/issues/391) [#487](https://github.com/miguelgrinberg/python-socketio/issues/487) [#447](https://github.com/miguelgrinberg/python-socketio/issues/447) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8d08096dc442e817b5e4ce53321ccf196daafcd1)) +- Fix multi-namespace disconnect logic [#456](https://github.com/miguelgrinberg/python-socketio/issues/456) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/76a9860abc11b5a18fa05e6a8fef815390642d09)) +- `AsyncPubSubManager` does not await for `can_disconnect()` [#488](https://github.com/miguelgrinberg/python-socketio/issues/488) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b0518936b949f759f2be8da0cc7b9c30af440371)) (thanks **Andrei Neagu**!) +- Require a recipient in `call()` function in the server [#476](https://github.com/miguelgrinberg/python-socketio/issues/476) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bbb9a7f0b630b6932960ae2545c62d6e5aee4f09)) (thanks **tt2468**!) +- ASGI startup and shutdown lifespan handlers [#468](https://github.com/miguelgrinberg/python-socketio/issues/468) Co-authored-by: avi ([commit](https://github.com/miguelgrinberg/python-socketio/commit/87d51dd1e6cde94adf4f8fb23828e1537c4f1301)) (thanks **databasedav**!) +- Remove references to Python 2.7 in the documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/02a7ce32c00ed5e64b0fae62d2d5ef93f25367df)) +- Fix server example in docstring [#449](https://github.com/miguelgrinberg/python-socketio/issues/449) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8caebfd524d77d118243878c4bd9d396c420e0a3)) (thanks **wangjiancn**!) +- Fix documentation typo [#450](https://github.com/miguelgrinberg/python-socketio/issues/450) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/cdde846e575c98d1316b1857b933bfeabf159580)) (thanks **kizError**!) + +**Release 4.5.1** - 2020-03-22 + +- Fix endless loop when disconnecting on multi-server deployments [#441](https://github.com/miguelgrinberg/python-socketio/issues/441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/16e873dbc7e100780c83a907a11299bd8269e5e3)) + +**Release 4.5.0** - 2020-03-14 + +- Add support for client disconnects in multi-server configurations [#1174](https://github.com/miguelgrinberg/Flask-SocketIO/issues/1174) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/01378ef1efca73330327006be467270462d504e0)) +- Initialize the client's SIGINT signal handler only if a client is created [#424](https://github.com/miguelgrinberg/python-socketio/issues/424) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/dc89963e328920a3756cefb508213c310ffa730c)) +- Fix for `Server` and `AsyncServer` when emitting no data [#420](https://github.com/miguelgrinberg/python-socketio/issues/420) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/e2242ce40e65c682e031d245db50fdd7956c3b2d)) (thanks **Aaron**!) +- More accurate logging documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d745477abf606f56f566f9d5b1b7bf9ffdb4fbc6)) +- `AsyncClient` documentation fixes [#389](https://github.com/miguelgrinberg/python-socketio/issues/389) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/aa2882cb3e2f3cda3a9d8c94b1c5db1bd0dbbf99)) (thanks **Dmitry Volodin**!) +- Document concurrency problems with emits [#403](https://github.com/miguelgrinberg/python-socketio/issues/403) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d972ca3a5476f5e4e9a114913bdd5f528e558a9f)) +- Minor documentation fixes [#386](https://github.com/miguelgrinberg/python-socketio/issues/386) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d4b403431152cbbbf31ff723562b069a36c330c4)) (thanks **Rotzbua**!) + **Release 4.4.0** - 2019-11-24 +- Last version to support Python 2 - Support the `connect_error` event in the client [#344](https://github.com/miguelgrinberg/python-socketio/issues/344) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/805d5f37413a1e3bbad22012237412803217b4b9)) - Do not dispatch events for disconnected namespaces [#333](https://github.com/miguelgrinberg/python-socketio/issues/333) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a839a36fa0fa7f0e5d8976ff47b217f6b1e8a44b)) - Fix documentation typos [#374](https://github.com/miguelgrinberg/python-socketio/issues/374) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b60bbc0307edd0bef2c8b11197ef5a04b2d11b71)) (thanks **Dmitry Volodin**!) diff --git a/MANIFEST.in b/MANIFEST.in index 64ad321d..10dd5123 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,5 @@ -include README.md LICENSE +include README.md LICENSE tox.ini +recursive-include docs * +recursive-exclude docs/_build * +recursive-include tests * +exclude **/*.pyc diff --git a/README.md b/README.md new file mode 100644 index 00000000..780465f4 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +python-socketio +=============== + +[![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) + +Python implementation of the `Socket.IO` realtime client and server. + +Sponsors +-------- + +The following organizations are funding this project: + +![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| +-|- + +Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? + +Version compatibility +--------------------- + +The Socket.IO protocol has been through a number of revisions, and some of these +introduced backward incompatible changes, which means that the client and the +server must use compatible versions for everything to work. + +If you are using the Python client and server, the easiest way to ensure compatibility +is to use the same version of this package for the client and the server. If you are +using this package with a different client or server, then you must ensure the +versions are compatible. + +The version compatibility chart below maps versions of this package to versions +of the JavaScript reference implementation and the versions of the Socket.IO and +Engine.IO protocols. + +JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version +-|-|-|- +0.9.x | 1, 2 | 1, 2 | Not supported +1.x and 2.x | 3, 4 | 3 | 4.x +3.x and 4.x | 5 | 4 | 5.x + +Resources +--------- + +- [Documentation](http://python-socketio.readthedocs.io/) +- [PyPI](https://pypi.python.org/pypi/python-socketio) +- [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) +- Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. diff --git a/README.rst b/README.rst deleted file mode 100644 index f16648ac..00000000 --- a/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -python-socketio -=============== - -.. image:: https://travis-ci.org/miguelgrinberg/python-socketio.svg?branch=master - :target: https://travis-ci.org/miguelgrinberg/python-socketio - -Python implementation of the `Socket.IO`_ realtime client and server. - -Resources ---------- - -- `Documentation`_ -- `PyPI`_ -- `Change Log`_ -- Questions? See the `questions`_ others have asked on Stack Overflow, or `ask`_ your own question. - -.. _Socket.IO: https://github.com/socketio/socket.io -.. _Documentation: http://python-socketio.readthedocs.io/en/latest/ -.. _PyPI: https://pypi.python.org/pypi/python-socketio -.. _Change Log: https://github.com/miguelgrinberg/python-socketio/blob/master/CHANGES.md -.. _questions: https://stackoverflow.com/questions/tagged/python-socketio -.. _ask: https://stackoverflow.com/questions/ask?tags=python+python-socketio diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..bfea1de3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at mailto:miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps. + +If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](http://pgp.mit.edu/pks/lookup?search=miguel.grinberg%40gmail.com&op=index). + +Please do not disclose vulnerabilities publicly before discussing how to proceed with me. diff --git a/bin/mkchangelog.py b/bin/mkchangelog.py deleted file mode 100644 index 0d0709ef..00000000 --- a/bin/mkchangelog.py +++ /dev/null @@ -1,49 +0,0 @@ -import datetime -import re -import sys -import git - -URL = 'https://github.com/miguelgrinberg/python-socketio' -merges = {} - - -def format_message(commit): - if commit.message.startswith('Version '): - return '' - if '#nolog' in commit.message: - return '' - if commit.message.startswith('Merge pull request'): - pr = commit.message.split('#')[1].split(' ')[0] - message = ' '.join([line for line in [line.strip() for line in commit.message.split('\n')[1:]] if line]) - merges[message] = pr - return '' - if commit.message.startswith('Release '): - return '\n**{message}** - {date}\n'.format( - message=commit.message.strip(), - date=datetime.datetime.fromtimestamp(commit.committed_date).strftime('%Y-%m-%d')) - message = ' '.join([line for line in [line.strip() for line in commit.message.split('\n')] if line]) - if message in merges: - message += ' #' + merges[message] - message = re.sub('\\(.*(#[0-9]+)\\)', '\\1', message) - message = re.sub('Fixes (#[0-9]+)', '\\1', message) - message = re.sub('fixes (#[0-9]+)', '\\1', message) - message = re.sub('#([0-9]+)', '[#\\1]({url}/issues/\\1)'.format(url=URL), message) - message += ' ([commit]({url}/commit/{sha}))'.format(url=URL, sha=str(commit)) - if commit.author.name != 'Miguel Grinberg': - message += ' (thanks **{name}**!)'.format(name=commit.author.name) - return '- ' + message - - -def main(all=False): - repo = git.Repo() - - for commit in repo.iter_commits(): - if not all and commit.message.startswith('Release '): - break - message = format_message(commit) - if message: - print(message) - - -if __name__ == '__main__': - main(all=len(sys.argv) > 1 and sys.argv[1] == 'all') diff --git a/bin/release b/bin/release deleted file mode 100755 index df181372..00000000 --- a/bin/release +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -ex - -VERSION="$1" -VERSION_FILE=socketio/__init__.py - -if [[ "$VERSION" == "" ]]; then - echo "Usage: $0 " - exit 1 -fi - -# update change log -head -n 2 CHANGES.md > _CHANGES.md -echo "**Release $VERSION** - $(date +%F)" >> _CHANGES.md -echo "" >> _CHANGES.md -pip install gitpython -python bin/mkchangelog.py >> _CHANGES.md -echo "" >> _CHANGES.md -len=$(wc -l < CHANGES.md) -tail -n $(expr $len - 2) CHANGES.md >> _CHANGES.md -vim _CHANGES.md -set +e -grep -q ABORT _CHANGES.md -if [[ "$?" == "0" ]]; then - rm _CHANGES.md - echo "Aborted." - exit 1 -fi -set -e -mv _CHANGES.md CHANGES.md - -sed -i "" "s/^__version__ = '.*'$/__version__ = '$VERSION'/" $VERSION_FILE -rm -rf dist -pip install --upgrade pip wheel twine -python setup.py sdist bdist_wheel --universal - -git add $VERSION_FILE CHANGES.md -git commit -m "Release $VERSION" -git tag -f v$VERSION -git push --tags origin master - -read -p "Press any key to submit to PyPI or Ctrl-C to abort..." -n1 -s -twine upload dist/* - -NEW_VERSION="${VERSION%.*}.$((${VERSION##*.}+1))dev" -sed -i "" "s/^__version__ = '.*'$/__version__ = '$NEW_VERSION'/" $VERSION_FILE -git add $VERSION_FILE -git commit -m "Version $NEW_VERSION" -git push origin master -echo "Development is now open on version $NEW_VERSION!" diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..9a862943 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,5 @@ +div.sphinxsidebar { + max-height: calc(100% - 30px); + overflow-y: auto; + overflow-x: hidden; +} diff --git a/docs/api.rst b/docs/api.rst index b23de1af..5a89bf60 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,129 +6,86 @@ API Reference .. module:: socketio -``Client`` class ----------------- +.. autoclass:: SimpleClient + :members: + :inherited-members: -.. autoclass:: Client +.. autoclass:: AsyncSimpleClient :members: + :inherited-members: -``AsyncClient`` class ---------------------- +.. autoclass:: Client + :members: + :inherited-members: .. autoclass:: AsyncClient :members: :inherited-members: -``Server`` class ----------------- - .. autoclass:: Server :members: - -``AsyncServer`` class ---------------------- + :inherited-members: .. autoclass:: AsyncServer :members: :inherited-members: -``ConnectionRefusedError`` class --------------------------------- - -.. autoclass:: ConnectionRefusedError +.. autoclass:: socketio.exceptions.ConnectionRefusedError :members: -``WSGIApp`` class ------------------ - .. autoclass:: WSGIApp :members: -``ASGIApp`` class ------------------ - .. autoclass:: ASGIApp :members: -``Middleware`` class (deprecated) ---------------------------------- - .. autoclass:: Middleware :members: -``ClientNamespace`` class -------------------------- - .. autoclass:: ClientNamespace :members: :inherited-members: -``Namespace`` class -------------------- - .. autoclass:: Namespace :members: :inherited-members: -``AsyncClientNamespace`` class ------------------------------- - .. autoclass:: AsyncClientNamespace :members: :inherited-members: -``AsyncNamespace`` class ------------------------- - .. autoclass:: AsyncNamespace :members: :inherited-members: -``BaseManager`` class ---------------------- - -.. autoclass:: BaseManager +.. autoclass:: Manager :members: - -``PubSubManager`` class ------------------------ + :inherited-members: .. autoclass:: PubSubManager :members: - -``KombuManager`` class ----------------------- + :inherited-members: .. autoclass:: KombuManager :members: - -``RedisManager`` class ----------------------- + :inherited-members: .. autoclass:: RedisManager :members: - -``KafkaManager`` class ----------------------- + :inherited-members: .. autoclass:: KafkaManager :members: - -``AsyncManager`` class ----------------------- + :inherited-members: .. autoclass:: AsyncManager :members: :inherited-members: -``AsyncRedisManager`` class ---------------------------- - .. autoclass:: AsyncRedisManager :members: - -``AsyncAioPikaManager`` class ---------------------------- + :inherited-members: .. autoclass:: AsyncAioPikaManager :members: + :inherited-members: diff --git a/docs/client.rst b/docs/client.rst index 80c66ca1..e3e1fb2c 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -1,15 +1,16 @@ -The Socket.IO Client -==================== +The Socket.IO Clients +===================== This package contains two Socket.IO clients: -- The :func:`socketio.Client` class creates a client compatible with the - standard Python library. -- The :func:`socketio.AsyncClient` class creates a client compatible with - the ``asyncio`` package. +- a "simple" client, which provides a straightforward API that is sufficient + for most applications +- an "event-driven" client, which provides access to all the features of the + Socket.IO protocol -The methods in the two clients are the same, with the only difference that in -the ``asyncio`` client most methods are implemented as coroutines. +Each of these clients comes in two variants: one for the standard Python +library, and another for asynchronous applications built with the ``asyncio`` +package. Installation ------------ @@ -23,8 +24,192 @@ If instead you plan on using the ``asyncio`` client, then use this:: pip install "python-socketio[asyncio_client]" +Using the Simple Client +----------------------- + +The advantage of the simple client is that it abstracts away the logic required +to maintain a Socket.IO connection. This client handles disconnections and +reconnections in a completely transparent way, without adding any complexity to +the application. + +Creating a Client Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to create a Socket.IO client is to use the context manager +interface:: + + import socketio + + # standard Python + with socketio.SimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + # asyncio + async with socketio.AsyncSimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + +With this usage the context manager will ensure that the client is properly +disconnected before exiting the ``with`` or ``async with`` block. + +If preferred, a client can be manually instantiated:: + + import socketio + + # standard Python + sio = socketio.SimpleClient() + + # asyncio + sio = socketio.AsyncSimpleClient() + +Connecting to a Server +~~~~~~~~~~~~~~~~~~~~~~ + +The connection to a server is established by calling the ``connect()`` +method:: + + sio.connect('http://localhost:5000') + +In the case of the ``asyncio`` client, the method is a coroutine:: + + await sio.connect('http://localhost:5000') + +By default the client first connects to the server using the long-polling +transport, and then attempts to upgrade the connection to use WebSocket. To +connect directly using WebSocket, use the ``transports`` argument:: + + sio.connect('http://localhost:5000', transports=['websocket']) + +Upon connection, the server assigns the client a unique session identifier. +The application can find this identifier in the ``sid`` attribute:: + + print('my sid is', sio.sid) + +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + +TLS/SSL Support +^^^^^^^^^^^^^^^ + +The client supports TLS/SSL connections. To enable it, use a ``https://`` +connection URL:: + + sio.connect('https://example.com') + +Or when using ``asyncio``:: + + await sio.connect('https://example.com') + +The client verifies server certificates by default. Consult the documentation +for the event-driven client for information on how to customize this behavior. + +Emitting Events +~~~~~~~~~~~~~~~ + +The client can emit an event to the server using the ``emit()`` method:: + + sio.emit('my message', {'foo': 'bar'}) + +Or in the case of ``asyncio``, as a coroutine:: + + await sio.emit('my message', {'foo': 'bar'}) + +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. + +Receiving Events +~~~~~~~~~~~~~~~~ + +The client can wait for the server to emit an event with the ``receive()`` +method:: + + event = sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +When using ``asyncio``, this method needs to be awaited:: + + event = await sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +The return value of ``receive()`` is a list. The first element of this list is +the event name, while the remaining elements are the arguments passed by the +server. + +With the usage shown above, the ``receive()`` method will return only when an +event is received from the server. An optional timeout in seconds can be passed +to prevent the client from waiting forever:: + + from socketio.exceptions import TimeoutError + + try: + event = sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Or with ``asyncio``:: + + from socketio.exceptions import TimeoutError + + try: + event = await sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Disconnecting from the Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At any time the client can request to be disconnected from the server by +invoking the ``disconnect()`` method:: + + sio.disconnect() + +For the ``asyncio`` client this is a coroutine:: + + await sio.disconnect() + +Debugging and Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To help you debug issues, the client can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Client(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncClient(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. + +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. + +Using the Event-Driven Client +----------------------------- + Creating a Client Instance --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ To instantiate an Socket.IO client, simply create an instance of the appropriate client class:: @@ -38,7 +223,7 @@ appropriate client class:: sio = socketio.AsyncClient() Defining Event Handlers ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The Socket.IO protocol is event based. When a server wants to communicate with a client it *emits* an event. Each event has a name, and a list of @@ -65,7 +250,56 @@ or can also be coroutines:: async def message(data): print('I received a message!') -The ``connect``, ``connect_error`` and ``disconnect`` events are special; they +If the server includes arguments with an event, those are passed to the +handler function as arguments. + +Catch-All Event and Namespace Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A "catch-all" event handler is invoked for any events that do not have an +event handler. You can define a catch-all handler using ``'*'`` as event name:: + + @sio.on('*') + def any_event(event, sid, data): + pass + +Asyncio servers can also use a coroutine:: + + @sio.on('*') + async def any_event(event, sid, data): + pass + +A catch-all event handler receives the event name as a first argument. The +remaining arguments are the same as for a regular event handler. + +The ``connect`` and ``disconnect`` events have to be defined explicitly and are +not invoked on a catch-all event handler. + +Similarily, a "catch-all" namespace handler is invoked for any connected +namespaces that do not have an explicitly defined event handler. As with +catch-all events, ``'*'`` is used in place of a namespace:: + + @sio.on('my_event', namespace='*') + def my_event_any_namespace(namespace, sid, data): + pass + +For these events, the namespace is passed as first argument, followed by the +regular arguments of the event. + +Lastly, it is also possible to define a "catch-all" handler for all events on +all namespaces:: + + @sio.on('*', namespace='*') + def any_event_any_namespace(event, namespace, sid, data): + pass + +Event handlers with catch-all events and namespaces receive the event name and +the namespace as first and second arguments. + +Connect, Connect Error and Disconnect Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``connect``, ``connect_error`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @@ -74,25 +308,43 @@ server:: print("I'm connected!") @sio.event - def connect_error(): + def connect_error(data): print("The connection failed!") @sio.event - def disconnect(): - print("I'm disconnected!") + def disconnect(reason): + print("I'm disconnected! reason:", reason) + +The ``connect_error`` handler is invoked when a connection attempt fails. If +the server provides arguments, these are passed on to the handler. The server +can use an argument to provide information to the client regarding the +connection failure. + +The ``disconnect`` handler is invoked for application initiated disconnects, +server initiated disconnects, or accidental disconnects, for example due to +networking failures. In the case of an accidental disconnection, the client is +going to attempt to reconnect immediately after invoking the disconnect +handler. As soon as the connection is re-established the connect handler will +be invoked once again. The handler receives a ``reason`` argument which +provides the cause of the disconnection:: + + @sio.event + def disconnect(reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) -Note that the ``disconnect`` handler is invoked for application initiated -disconnects, server initiated disconnects, or accidental disconnects, for -example due to networking failures. In the case of an accidental -disconnection, the client is going to attempt to reconnect immediately after -invoking the disconnect handler. As soon as the connection is re-established -the connect handler will be invoked once again. +See the The :attr:`socketio.Client.reason` attribute for a list of possible +disconnection reasons. -If the server includes arguments with an event, those are passed to the -handler function as arguments. +The ``connect``, ``connect_error`` and ``disconnect`` events have to be +defined explicitly and are not invoked on a catch-all event handler. Connecting to a Server ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ The connection to a server is established by calling the ``connect()`` method:: @@ -104,12 +356,87 @@ In the case of the ``asyncio`` client, the method is a coroutine:: await sio.connect('http://localhost:5000') Upon connection, the server assigns the client a unique session identifier. -The applicaction can find this identifier in the ``sid`` attribute:: +The application can find this identifier in the ``sid`` attribute:: print('my sid is', sio.sid) +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + +TLS/SSL Support +^^^^^^^^^^^^^^^ + +The client supports TLS/SSL connections. To enable it, use a ``https://`` +connection URL:: + + sio.connect('https://example.com') + +Or when using ``asyncio``:: + + await sio.connect('https://example.com') + +The client will verify the server certificate by default. To disable +certificate verification, or to use other less common options such as client +certificates, the client must be initialized with a custom HTTP session object +that is configured with the desired TLS/SSL options. + +The following example disables server certificate verification, which can be +useful when connecting to a server that uses a self-signed certificate:: + + http_session = requests.Session() + http_session.verify = False + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And when using ``asyncio``:: + + connector = aiohttp.TCPConnector(ssl=False) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + +Instead of disabling certificate verification, you can provide a custom +certificate authority bundle to verify the certificate against:: + + http_session = requests.Session() + http_session.verify = '/path/to/ca.pem' + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And for ``asyncio``:: + + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations('/path/to/ca.pem') + connector = aiohttp.TCPConnector(ssl=ssl_context) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + +Below you can see how to use a client certificate to authenticate against the +server:: + + http_session = requests.Session() + http_session.cert = ('/path/to/client/cert.pem', '/path/to/client/key.pem') + sio = socketio.Client(http_session=http_session) + sio.connect('https://example.com') + +And for ``asyncio``:: + + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain('/path/to/client/cert.pem', + '/path/to/client/key.pem') + connector = aiohttp.TCPConnector(ssl=ssl_context) + http_session = aiohttp.ClientSession(connector=connector) + sio = socketio.AsyncClient(http_session=http_session) + await sio.connect('https://example.com') + Emitting Events ---------------- +~~~~~~~~~~~~~~~ The client can emit an event to the server using the ``emit()`` method:: @@ -119,18 +446,19 @@ Or in the case of ``asyncio``, as a coroutine:: await sio.emit('my message', {'foo': 'bar'}) -The single argument provided to the method is the data that is passed on -to the server. The data can be of type ``str``, ``bytes``, ``dict``, -``list`` or ``tuple``. When sending a ``tuple``, the elements in it need to -be of any of the other four allowed types. The elements of the tuple will be -passed as multiple arguments to the server-side event handler function. +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. The ``emit()`` method can be invoked inside an event handler as a response to a server event, or in any other part of the application, including in background tasks. Event Callbacks ---------------- +~~~~~~~~~~~~~~~ When a server emits an event to a client, it can optionally provide a callback function, to be invoked as a way of acknowledgment that the server @@ -152,7 +480,7 @@ the event, and any values returned by the server handler will be passed as arguments to this function. Namespaces ----------- +~~~~~~~~~~ The Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection. Clients can open multiple connections by @@ -184,7 +512,7 @@ If the ``namespaces`` argument of the ``connect()`` call isn't given, any namespaces used in event handlers are automatically connected. Class-Based Namespaces ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ As an alternative to the decorator-based event handlers, the event handlers that belong to a namespace can be created as methods of a subclass of @@ -194,7 +522,7 @@ that belong to a namespace can be created as methods of a subclass of def on_connect(self): pass - def on_disconnect(self): + def on_disconnect(self, reason): pass def on_my_event(self, data): @@ -210,7 +538,7 @@ coroutines if desired:: def on_connect(self): pass - def on_disconnect(self): + def on_disconnect(self, reason): pass async def on_my_event(self, data): @@ -218,6 +546,11 @@ coroutines if desired:: sio.register_namespace(MyCustomNamespace('/chat')) +A catch-all class-based namespace handler can be defined by passing ``'*'`` as +the namespace during registration:: + + sio.register_namespace(MyCustomNamespace('*')) + When class-based namespaces are used, any events received by the client are dispatched to a method named as the event name with the ``on_`` prefix. For example, event ``my_event`` will be handled by a method named ``on_my_event``. @@ -235,7 +568,7 @@ decorator-based function handler, only the standalone function handler is invoked. Disconnecting from the Server ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ At any time the client can request to be disconnected from the server by invoking the ``disconnect()`` method:: @@ -247,7 +580,7 @@ For the ``asyncio`` client this is a coroutine:: await sio.disconnect() Managing Background Tasks -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ When a client connection to the server is established, a few background tasks will be spawned to keep the connection alive and handle incoming @@ -272,7 +605,7 @@ start a custom background task:: # do some background work here! pass - sio.start_background_task(my_background_task, 123) + task = sio.start_background_task(my_background_task, 123) The arguments passed to this method are the background function and any positional or keyword arguments to invoke the function with. @@ -283,12 +616,12 @@ Here is the ``asyncio`` version:: # do some background work here! pass - sio.start_background_task(my_background_task, 123) + task = sio.start_background_task(my_background_task, 123) Note that this function is not a coroutine, since it does not wait for the background function to end. The background function must be a coroutine. -The ``sleep()`` method is a second convenince function that is provided for +The ``sleep()`` method is a second convenience function that is provided for the benefit of applications working with background tasks of their own:: sio.sleep(2) @@ -299,3 +632,26 @@ Or for ``asyncio``:: The single argument passed to the method is the number of seconds to sleep for. + +Debugging and Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To help you debug issues, the client can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Client(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncClient(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. + +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. diff --git a/docs/conf.py b/docs/conf.py index e8398c3d..b5d242bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,8 @@ 'sphinx.ext.autodoc', ] +autodoc_member_order = 'alphabetical' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -59,7 +61,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/intro.rst b/docs/intro.rst index 7f58c012..9bb301df 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -15,6 +15,32 @@ web browsers) and a server. The official implementations of the client and server components are written in JavaScript. This package provides Python implementations of both, each with standard and asyncio variants. +Version compatibility +--------------------- + +The Socket.IO protocol has been through a number of revisions, and some of these +introduced backward incompatible changes, which means that the client and the +server must use compatible versions for everything to work. + +If you are using the Python client and server, the easiest way to ensure compatibility +is to use the same version of this package for the client and the server. If you are +using this package with a different client or server, then you must ensure the +versions are compatible. + +The version compatibility chart below maps versions of this package to versions +of the JavaScript reference implementation and the versions of the Socket.IO and +Engine.IO protocols. + ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version | python-engineio version | ++==============================+=============================+=============================+=========================+=========================+ +| 0.9.x | 1, 2 | 1, 2 | Not supported | Not supported | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| 1.x and 2.x | 3, 4 | 3 | 4.x | 3.x | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ +| 3.x and 4.x | 5 | 4 | 5.x | 4.x | ++------------------------------+-----------------------------+-----------------------------+-------------------------+-------------------------+ + Client Examples --------------- @@ -42,12 +68,41 @@ The example that follows shows a simple Python client: sio.connect('http://localhost:5000') sio.wait() +Below is a similar client, coded for ``asyncio`` (Python 3.5+ only): + +.. code:: python + + import asyncio + import socketio + + sio = socketio.AsyncClient() + + @sio.event + async def connect(): + print('connection established') + + @sio.event + async def my_message(data): + print('message received with ', data) + await sio.emit('my response', {'response': 'my response'}) + + @sio.event + async def disconnect(): + print('disconnected from server') + + async def main(): + await sio.connect('http://localhost:5000') + await sio.wait() + + if __name__ == '__main__': + asyncio.run(main()) + Client Features --------------- -- Can connect to other Socket.IO compliant servers besides the one in - this package. -- Compatible with Python 2.7 and 3.5+. +- Can connect to other Socket.IO servers that are compatible with the + JavaScript Socket.IO reference server. +- Compatible with Python 3.8+. - Two versions of the client, one for standard Python and another for asyncio. - Uses an event-based architecture implemented with decorators that @@ -110,7 +165,6 @@ Uvicorn web server: @sio.event async def chat_message(sid, data): print("message ", data) - await sio.emit('reply', room=sid) @sio.event def disconnect(sid): @@ -125,22 +179,22 @@ Uvicorn web server: Server Features --------------- -- Can connect to servers running other compliant Socket.IO clients besides - the one in this package. -- Compatible with Python 2.7 and Python 3.5+. +- Can connect to servers running other Socket.IO clients that are compatible + with the JavaScript reference client. +- Compatible with Python 3.8+. - Two versions of the server, one for standard Python and another for asyncio. - Supports large number of clients even on modest hardware due to being asynchronous. -- Can be hosted on any `WSGI `_ and - `ASGI `_ web servers includind +- Can be hosted on any `WSGI `_ or + `ASGI `_ web server including `Gunicorn `_, `Uvicorn `_, - `eventlet `_ and `gevent `_. + `eventlet `_ and `gevent `_. - Can be integrated with WSGI applications written in frameworks such as Flask, Django, etc. - Can be integrated with `aiohttp `_, - `sanic `_ and `tornado `_ - ``asyncio`` applications. + `FastAPI `_, `sanic `_ + and `tornado `_ ``asyncio`` applications. - Broadcasting of messages to all connected clients, or to subsets of them assigned to "rooms". - Optional support for multiple servers, connected through a messaging queue diff --git a/docs/server.rst b/docs/server.rst index faf77e8a..9432a172 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -19,47 +19,76 @@ command:: pip install python-socketio -In addition to the server, you will need to select an asynchronous framework -or server to use along with it. The list of supported packages is covered -in the :ref:`deployment-strategies` section. - Creating a Server Instance -------------------------- -A Socket.IO server is an instance of class :class:`socketio.Server`. This -instance can be transformed into a standard WSGI application by wrapping it -with the :class:`socketio.WSGIApp` class:: - - import socketio +A Socket.IO server is an instance of class :class:`socketio.Server`:: - # create a Socket.IO server - sio = socketio.Server() + import socketio - # wrap with a WSGI application - app = socketio.WSGIApp(sio) + # create a Socket.IO server + sio = socketio.Server() For asyncio based servers, the :class:`socketio.AsyncServer` class provides -the same functionality, but in a coroutine friendly format. If desired, The -:class:`socketio.ASGIApp` class can transform the server into a standard -ASGI application:: +the same functionality, but in a coroutine friendly format:: + + import socketio # create a Socket.IO server sio = socketio.AsyncServer() +Running the Server +------------------ + +To run the Socket.IO application it is necessary to configure a web server to +receive incoming requests from clients and forward them to the Socket.IO +server instance. To simplify this task, several integrations are available, +including support for the `WSGI `_ +and `ASGI `_ standards. + +Running as a WSGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as a WSGI application wrap the server +instance with the :class:`socketio.WSGIApp` class:: + + # wrap with a WSGI application + app = socketio.WSGIApp(sio) + +The resulting WSGI application can be executed with supported WSGI servers +such as `Werkzeug `_ for development and +`Gunicorn `_ for production. + +When combining Socket.IO with a web application written with a WSGI framework +such as Flask or Django, the ``WSGIApp`` class can wrap both applications +together and route traffic to them:: + + from mywebapp import app # a Flask, Django, etc. application + app = socketio.WSGIApp(sio, app) + +Running as an ASGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as an ASGI application wrap the server +instance with the :class:`socketio.ASGIApp` class:: + # wrap with ASGI application app = socketio.ASGIApp(sio) -These two wrappers can also act as middlewares, forwarding any traffic that is -not intended to the Socket.IO server to another application. This allows -Socket.IO servers to integrate easily into existing WSGI or ASGI applications:: +The resulting ASGI application can be executed with an ASGI compliant web +server, for example `Uvicorn `_. + +Socket.IO can also be combined with a web application written with an ASGI +web framework such as FastAPI. In that case, the ``ASGIApp`` class can wrap +both applications together and route traffic to them:: - from wsgi import app # a Flask, Django, etc. application - app = socketio.WSGIApp(sio, app) + from mywebapp import app # a FastAPI or other ASGI application + app = socketio.ASGIApp(sio, app) Serving Static Files --------------------- +~~~~~~~~~~~~~~~~~~~~ -The Engine.IO server can be configured to serve static files to clients. This +The Socket.IO server can be configured to serve static files to clients. This is particularly useful to deliver HTML, CSS and JavaScript files to clients when this package is used without a companion web framework. @@ -91,8 +120,8 @@ If desired, an explicit content type for a static file can be given as follows:: '/': {'filename': 'latency.html', 'content_type': 'text/plain'}, } -It is also possible to configure an entire directory in a single rule, so that all -the files in it are served as static files:: +It is also possible to configure an entire directory in a single rule, so that +all the files in it are served as static files:: static_files = { '/static': './public', @@ -147,13 +176,21 @@ Note: static file serving is intended for development use only, and as such it lacks important features such as caching. Do not use in a production environment. -Defining Event Handlers ------------------------ +Events +------ The Socket.IO protocol is event based. When a client wants to communicate with -the server it *emits* an event. Each event has a name, and a list of -arguments. The server registers event handler functions with the -:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators:: +the server, or the server wants to communicate with one or more clients, they +*emit* an event to the other party. Each event has a name, and an optional list +of arguments. + +Listening to Events +~~~~~~~~~~~~~~~~~~~ + +To receive events from clients, the server application must register event +handler functions. These functions are invoked when the corresponding events +are emitted by clients. To register a handler for an event, the +:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators are used:: @sio.event def my_event(sid, data): @@ -174,39 +211,84 @@ For asyncio servers, event handlers can optionally be given as coroutines:: async def my_event(sid, data): pass -The ``sid`` argument is the Socket.IO session id, a unique identifier of each -client connection. All the events sent by a given client will have the same -``sid`` value. +The ``sid`` argument that is passed to all handlers is the Socket.IO session +id, a unique identifier that Socket.IO assigns to each client connection. All +the events sent by a given client will have the same ``sid`` value. + +Connect and Disconnect Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``connect`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @sio.event - def connect(sid, environ): + def connect(sid, environ, auth): print('connect ', sid) @sio.event - def disconnect(sid): - print('disconnect ', sid) + def disconnect(sid, reason): + print('disconnect ', sid, reason) The ``connect`` event is an ideal place to perform user authentication, and any necessary mapping between user entities in the application and the ``sid`` -that was assigned to the client. The ``environ`` argument is a dictionary in -standard WSGI format containing the request information, including HTTP -headers. After inspecting the request, the connect event handler can return -``False`` to reject the connection with the client. +that was assigned to the client. -Sometimes it is useful to pass data back to the client being rejected. In that -case instead of returning ``False`` -:class:`socketio.exceptions.ConnectionRefusedError` can be raised, and all of -its arguments will be sent to the client with the rejection message:: +In addition to the ``sid``, the connect handler receives ``environ`` as an +argument, with the request information in standard WSGI format, including HTTP +headers. The connect handler also receives the ``auth`` argument with any +authentication details passed by the client, or ``None`` if the client did not +pass any authentication. + +After inspecting the arguments, the connect event handler can return ``False`` +to reject the connection with the client. Sometimes it is useful to pass data +back to the client being rejected. In that case instead of returning ``False`` +a :class:`socketio.exceptions.ConnectionRefusedError` exception can be raised, +and all of its arguments will be sent to the client with the rejection +message:: @sio.event - def connect(sid, environ): + def connect(sid, environ, auth): raise ConnectionRefusedError('authentication failed') -Emitting Events ---------------- +The disconnect handler receives the ``sid`` assigned to the client and a +``reason``, which provides the cause of the disconnection:: + + @sio.event + def disconnect(sid, reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) + +See the The :attr:`socketio.Server.reason` attribute for a list of possible +disconnection reasons. + +Catch-All Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~~ + +A "catch-all" event handler is invoked for any events that do not have an +event handler. You can define a catch-all handler using ``'*'`` as event name:: + + @sio.on('*') + def any_event(event, sid, data): + pass + +Asyncio servers can also use a coroutine:: + + @sio.on('*') + async def any_event(event, sid, data): + pass + +A catch-all event handler receives the event name as a first argument. The +remaining arguments are the same as for a regular event handler. + +Note that the ``connect`` and ``disconnect`` events have to be defined +explicitly and are not invoked on a catch-all event handler. + +Emitting Events to Clients +~~~~~~~~~~~~~~~~~~~~~~~~~~ Socket.IO is a bidirectional protocol, so at any time the server can send an event to its connected clients. The :func:`socketio.Server.emit` method is @@ -214,42 +296,102 @@ used for this task:: sio.emit('my event', {'data': 'foobar'}) -Sometimes the server may want to send an event just to a particular client. -This can be achieved by adding a ``room`` argument to the emit call:: +The first argument is the event name, followed by an optional data payload of +type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``. When sending a +``list``, ``dict`` or ``tuple``, the elements are also constrained to the same +data types. When a ``tuple`` is sent, the elements of the tuple will be passed +as multiple arguments to the client-side event handler function. - sio.emit('my event', {'data': 'foobar'}, room=user_sid) +The above example will send the event to all the clients are connected. +Sometimes the server may want to send an event just to one particular client. +This can be achieved by adding a ``to`` argument to the emit call, with the +``sid`` of the client:: -The :func:`socketio.Server.emit` method takes an event name, a message payload -of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient -room. When sending a ``tuple``, the elements in it need to be of any of the -other four allowed types. The elements of the tuple will be passed as multiple -arguments to the client-side event handler function. The ``room`` argument is -used to identify the client that should receive the event, and is set to the -``sid`` value assigned to that client's connection with the server. When -omitted, the event is broadcasted to all connected clients. + sio.emit('my event', {'data': 'foobar'}, to=user_sid) -Event Callbacks ---------------- +The ``to`` argument is used to identify the client that should receive the +event, and is set to the ``sid`` value assigned to that client's connection +with the server. When ``to`` is omitted, the event is broadcasted to all +connected clients. -When a client sends an event to the server, it can optionally provide a -callback function, to be invoked as a way of acknowledgment that the server -has processed the event. While this is entirely managed by the client, the -server can provide a list of values that are to be passed on to the callback -function, simply by returning them from the handler function:: +Acknowledging Events +~~~~~~~~~~~~~~~~~~~~ + +When a client sends an event to the server, it can optionally request to +receive acknowledgment from the server. The sending of acknowledgements is +automatically managed by the Socket.IO server, but the event handler function +can provide a list of values that are to be passed on to the client with the +acknowledgement simply by returning them:: @sio.event def my_event(sid, data): # handle the message - return "OK", 123 + return "OK", 123 # <-- client will have these as acknowledgement + +Requesting Client Acknowledgements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to how clients can request acknowledgements from the server, when the +server is emitting to a single client it can also ask the client to acknowledge +the event, and optionally return one or more values as a response. + +The Socket.IO server supports two ways of working with client acknowledgements. +The most convenient method is to replace :func:`socketio.Server.emit` with +:func:`socketio.Server.call`. The ``call()`` method will emit the event, and +then wait until the client sends an acknowledgement, returning any values +provided by the client:: + + response = sio.call('my event', {'data': 'foobar'}, to=user_sid) + +A much more primitive acknowledgement solution uses callback functions. The +:func:`socketio.Server.emit` method has an optional ``callback`` argument that +can be set to a callable. If this argument is given, the callable will be +invoked after the client has processed the event, and any values returned by +the client will be passed as arguments to this function:: + + def my_callback(): + print("callback invoked!") + + sio.emit('my event', {'data': 'foobar'}, to=user_sid, callback=my_callback) + +Rooms +----- + +To make it easy for the server to emit events to groups of related clients, +the application can put its clients into "rooms", and then address messages to +these rooms. + +In previous examples, the ``to`` argument of the :func:`socketio.SocketIO.emit` +method was used to designate a specific client as the recipient of the event. +The ``to`` argument can also be given the name of a room, and then all the +clients that are in that room will receive the event. + +The application can create as many rooms as needed and manage which clients are +in them using the :func:`socketio.Server.enter_room` and +:func:`socketio.Server.leave_room` methods. Clients can be in as many +rooms as needed and can be moved between rooms when necessary. + +:: + + @sio.event + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') -Likewise, the server can request a callback function to be invoked after a -client has processed an event. The :func:`socketio.Server.emit` method has an -optional ``callback`` argument that can be set to a callable. If this -argument is given, the callable will be invoked after the client has processed -the event, and any values returned by the client will be passed as arguments -to this function. Using callback functions when broadcasting to multiple -clients is not recommended, as the callback function will be invoked once for -each client that received the message. + @sio.event + def exit_chat(sid): + sio.leave_room(sid, 'chat_users') + +In chat applications it is often desired that an event is broadcasted to all +the members of the room except one, which is the originator of the event such +as a chat message. The :func:`socketio.Server.emit` method provides an +optional ``skip_sid`` argument to indicate a client that should be skipped +during the broadcast. + +:: + + @sio.event + def my_message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) Namespaces ---------- @@ -261,11 +403,15 @@ as a pathname following the hostname and port. For example, connecting to *http://example.com:8000/chat* would open a connection to the namespace */chat*. -Each namespace is handled independently from the others, with separate session -IDs (``sid``\ s), event handlers and rooms. It is important that applications -that use multiple namespaces specify the correct namespace when setting up -their event handlers and rooms, using the optional ``namespace`` argument -available in all the methods in the :class:`socketio.Server` class:: +Each namespace works independently from the others, with separate session +IDs (``sid``\ s), event handlers and rooms. Namespaces can be defined directly +in the event handler functions, or they can also be created as classes. + +Decorator-Based Namespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Decorator-based namespaces are regular event handlers that include the +``namespace`` argument in their decorator:: @sio.event(namespace='/chat') def my_custom_event(sid, data): @@ -279,18 +425,24 @@ When emitting an event, the ``namespace`` optional argument is used to specify which namespace to send it on. When the ``namespace`` argument is omitted, the default Socket.IO namespace, which is named ``/``, is used. +It is important that applications that use multiple namespaces specify the +correct namespace when setting up their event handlers and rooms using the +optional ``namespace`` argument. This argument must also be specified when +emitting events under a namespace. Most methods in the :class:`socketio.Server` +class have the optional ``namespace`` argument. + Class-Based Namespaces ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ -As an alternative to the decorator-based event handlers, the event handlers -that belong to a namespace can be created as methods of a subclass of +As an alternative to the decorator-based namespaces, the event handlers that +belong to a namespace can be created as methods in a subclass of :class:`socketio.Namespace`:: class MyCustomNamespace(socketio.Namespace): def on_connect(self, sid, environ): pass - def on_disconnect(self, sid): + def on_disconnect(self, sid, reason): pass def on_my_event(self, sid, data): @@ -298,7 +450,7 @@ that belong to a namespace can be created as methods of a subclass of sio.register_namespace(MyCustomNamespace('/test')) -For asyncio based severs, namespaces must inherit from +For asyncio based servers, namespaces must inherit from :class:`socketio.AsyncNamespace`, and can define event handlers as coroutines if desired:: @@ -306,7 +458,7 @@ if desired:: def on_connect(self, sid, environ): pass - def on_disconnect(self, sid): + def on_disconnect(self, sid, reason): pass async def on_my_event(self, sid, data): @@ -335,43 +487,35 @@ that a single instance of a namespace class is used for all clients, and consequently, a namespace instance cannot be used to store client specific information. -Rooms ------ +Catch-All Namespaces +~~~~~~~~~~~~~~~~~~~~ -To make it easy for the server to emit events to groups of related clients, -the application can put its clients into "rooms", and then address messages to -these rooms. +Similarily to catch-all event handlers, a "catch-all" namespace can be used +when defining event handlers for any connected namespaces that do not have an +explicitly defined event handler. As with catch-all events, ``'*'`` is used in +place of a namespace:: -In the previous section the ``room`` argument of the -:func:`socketio.SocketIO.emit` method was used to designate a specific -client as the recipient of the event. This is because upon connection, a -personal room for each client is created and named with the ``sid`` assigned -to the connection. The application is then free to create additional rooms and -manage which clients are in them using the :func:`socketio.Server.enter_room` -and :func:`socketio.Server.leave_room` methods. Clients can be in as many -rooms as needed and can be moved between rooms as often as necessary. + @sio.on('my_event', namespace='*') + def my_event_any_namespace(namespace, sid, data): + pass -:: +For these events, the namespace is passed as first argument, followed by the +regular arguments of the event. - @sio.event - def begin_chat(sid): - sio.enter_room(sid, 'chat_users') +A catch-all class-based namespace handler can be defined by passing ``'*'`` as +the namespace during registration:: - @sio.event - def exit_chat(sid): - sio.leave_room(sid, 'chat_users') + sio.register_namespace(MyCustomNamespace('*')) -In chat applications it is often desired that an event is broadcasted to all -the members of the room except one, which is the originator of the event such -as a chat message. The :func:`socketio.Server.emit` method provides an -optional ``skip_sid`` argument to indicate a client that should be skipped -during the broadcast. +A "catch-all" handler for all events on all namespaces can be defined as +follows:: -:: + @sio.on('*', namespace='*') + def any_event_any_namespace(event, namespace, sid, data): + pass - @sio.event - def my_message(sid, data): - sio.emit('my reply', data, room='chat_users', skip_sid=sid) +Event handlers with catch-all events and namespaces receive the event name and +the namespace as first and second arguments. User Sessions ------------- @@ -422,13 +566,13 @@ The session can also be manipulated with the `session()` context manager:: For the ``asyncio`` server, an asynchronous context manager is used:: @sio.event - def connect(sid, environ): + async def connect(sid, environ): username = authenticate_user(environ) async with sio.session(sid) as session: session['username'] = username @sio.event - def message(sid, data): + async def message(sid, data): async with sio.session(sid) as session: print('message from ', session['username']) @@ -440,254 +584,362 @@ Note: the contents of the user session are destroyed when the client disconnects. In particular, user session contents are not preserved when a client reconnects after an unexpected disconnection from the server. -Using a Message Queue +Cross-Origin Controls --------------------- -When working with distributed applications, it is often necessary to access -the functionality of the Socket.IO from multiple processes. There are two -specific use cases: +For security reasons, this server enforces a same-origin policy by default. In +practical terms, this means the following: -- Applications that use a work queues such as - `Celery `_ may need to emit an event to a - client once a background job completes. The most convenient place to carry - out this task is the worker process that handled this job. +- If an incoming HTTP or WebSocket request includes the ``Origin`` header, + this header must match the scheme and host of the connection URL. In case + of a mismatch, a 400 status code response is returned and the connection is + rejected. +- No restrictions are imposed on incoming requests that do not include the + ``Origin`` header. -- Highly available applications may want to use horizontal scaling of the - Socket.IO server to be able to handle very large number of concurrent - clients. +If necessary, the ``cors_allowed_origins`` option can be used to allow other +origins. This argument can be set to a string to set a single allowed origin, or +to a list to allow multiple origins. A special value of ``'*'`` can be used to +instruct the server to allow all origins, but this should be done with care, as +this could make the server vulnerable to Cross-Site Request Forgery (CSRF) +attacks. -As a solution to the above problems, the Socket.IO server can be configured -to connect to a message queue such as `Redis `_ or -`RabbitMQ `_, to communicate with other related -Socket.IO servers or auxiliary workers. +Monitoring and Administration +----------------------------- -Redis -~~~~~ +The Socket.IO server can be configured to accept connections from the official +`Socket.IO Admin UI `_. This tool provides +real-time information about currently connected clients, rooms in use and +events being emitted. It also allows an administrator to manually emit events, +change room assignments and disconnect clients. The hosted version of this tool +is available at `https://admin.socket.io `_. -To use a Redis message queue, a Python Redis client must be installed:: +Given that enabling this feature can affect the performance of the server, it +is disabled by default. To enable it, call the +:func:`instrument() ` method. For example:: - # socketio.Server class - pip install redis + import os + import socketio - # socketio.AsyncServer class - pip install aioredis + sio = socketio.Server(cors_allowed_origins=[ + 'http://localhost:5000', + 'https://admin.socket.io', + ]) + sio.instrument(auth={ + 'username': 'admin', + 'password': os.environ['ADMIN_PASSWORD'], + }) -The Redis queue is configured through the :class:`socketio.RedisManager` and -:class:`socketio.AsyncRedisManager` classes. These classes connect directly to -the Redis store and use the queue's pub/sub functionality:: +This configures the server to accept connections from the hosted Admin UI +client. Administrators can then open https://admin.socket.io in their web +browsers and log in with username ``admin`` and the password given by the +``ADMIN_PASSWORD`` environment variable. To ensure the Admin UI front end is +allowed to connect, CORS is also configured. - # socketio.Server class - mgr = socketio.RedisManager('redis://') - sio = socketio.Server(client_manager=mgr) +Consult the reference documentation to learn about additional configuration +options that are available. - # socketio.AsyncServer class - mgr = socketio.AsyncRedisManager('redis://') - sio = socketio.AsyncServer(client_manager=mgr) +Debugging and Troubleshooting +----------------------------- -The ``client_manager`` argument instructs the server to connect to the given -message queue, and to coordinate with other processes connected to the queue. +To help you debug issues, the server can be configured to output logs to the +terminal:: -Kombu -~~~~~ + import socketio -`Kombu `_ is a Python package that -provides access to RabbitMQ and many other message queues. It can be installed -with pip:: + # standard Python + sio = socketio.Server(logger=True, engineio_logger=True) - pip install kombu + # asyncio + sio = socketio.AsyncServer(logger=True, engineio_logger=True) -To use RabbitMQ or other AMQP protocol compatible queues, that is the only -required dependency. But for other message queues, Kombu may require -additional packages. For example, to use a Redis queue via Kombu, the Python -package for Redis needs to be installed as well:: +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. - pip install redis +Logging can help identify the cause of connection problems, 400 responses, +bad performance and other issues. -The queue is configured through the :class:`socketio.KombuManager`:: +Concurrency and Web Server Integration +-------------------------------------- - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) +The Socket.IO server can be configured with different concurrency models +depending on the needs of the application and the web server that is used. The +concurrency model is given by the ``async_mode`` argument in the server. For +example:: -The connection URL passed to the :class:`KombuManager` constructor is passed -directly to Kombu's `Connection object -`_, so -the Kombu documentation should be consulted for information on how to build -the correct URL for a given message queue. + sio = socketio.Server(async_mode='threading') -Note that Kombu currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. +The following sub-sections describe the available concurrency options for +synchronous and asynchronous servers. + +Standard Modes +~~~~~~~~~~~~~~ + +- ``threading``: the server will use Python threads for concurrency and will + run on any multi-threaded WSGI server. This is the default mode when no other + concurrency libraries are installed. +- ``gevent``: the server will use greenlets through the + `gevent `_ library for concurrency. A web server that + is compatible with ``gevent`` is required. +- ``gevent_uwsgi``: a variation of the ``gevent`` mode that is designed to work + with the `uWSGI `_ web server. +- ``eventlet``: the server will use greenlets through the + `eventlet `_ library for concurrency. A web server that + is compatible with ``eventlet`` is required. Use of ``eventlet`` is not + recommended due to this project being in maintenance mode. + +Asyncio Modes +~~~~~~~~~~~~~ + +The asynchronous options are all based on the +`asyncio `_ package of the +Python standard library, with minor variations depending on the web server +platform that is used. + +- ``asgi``: use of any + `ASGI `_ web server is required. +- ``aiohttp``: use of the `aiohttp `_ web + framework and server is required. +- ``tornado``: use of the `Tornado `_ web framework + and server is required. +- ``sanic``: use of the `Sanic `_ web framework + and server is required. When using Sanic, it is recommended to use the + ``asgi`` mode instead. -Kafka -~~~~~ +.. _deployment-strategies: -`Apache Kafka `_ is supported through the -`kafka-python `_ -package:: +Deployment Strategies +--------------------- - pip install kafka-python +The following sections describe a variety of deployment strategies for +Socket.IO servers. -Access to Kafka is configured through the :class:`socketio.KafkaManager` -class:: +Gunicorn +~~~~~~~~ - mgr = socketio.KafkaManager('kafka://') - sio = socketio.Server(client_manager=mgr) +The simplest deployment strategy for the Socket.IO server is to use the popular +`Gunicorn `_ web server in multi-threaded mode. The +Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: -Note that Kafka currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio) -AioPika -~~~~~~~ +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: -A RabbitMQ message queue is supported in asyncio applications through the -`AioPika `_ package:: -You need to install aio_pika with pip:: + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio, other_wsgi_app) - pip install aio_pika +The example that follows shows how to start a Socket.IO application using +Gunicorn's threaded worker class:: -The RabbitMQ queue is configured through the -:class:`socketio.AsyncAioPikaManager` class:: + $ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app - mgr = socketio.AsyncAioPikaManager('amqp://') - sio = socketio.AsyncServer(client_manager=mgr) +With the above configuration the server will be able to handle close to 100 +concurrent clients. -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +It is also possible to use more than one worker process, but this has two +additional requirements: -To have a process other than a server connect to the queue to emit a message, -the same client manager classes can be used as standalone objects. In this -case, the ``write_only`` argument should be set to ``True`` to disable the -creation of a listening thread, which only makes sense in a server. For -example:: +- The clients must connect directly over WebSocket. The long-polling transport + is incompatible with the way Gunicorn load balances requests among workers. + To disable long-polling in the server, add ``transports=['websocket']`` in + the server constructor. Clients will have a similar option to initiate the + connection with WebSocket. +- The :func:`socketio.Server` instances in each worker must be configured with + a message queue to allow the workers to communicate with each other. See the + :ref:`using-a-message-queue` section for more information. - # connect to the redis queue as an external process - external_sio = socketio.RedisManager('redis://', write_only=True) +When using multiple workers, the approximate number of connections the server +will be able to accept can be calculated as the number of workers multiplied by +the number of threads per worker. - # emit an event - external_sio.emit('my event', data={'foo': 'bar'}, room='my room') +Note that Gunicorn can also be used alongside ``uvicorn``, ``gevent`` and +``eventlet``. These options are discussed under the appropriate sections below. -.. _deployment-strategies: +Uvicorn (and other ASGI web servers) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Deployment Strategies ---------------------- +When working with an asynchronous Socket.IO server, the easiest deployment +strategy is to use an ASGI web server such as +`Uvicorn `_. -The following sections describe a variety of deployment strategies for -Socket.IO servers. +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to a ``socketio.AsyncServer`` instance:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) + +If desired, the ``socketio.ASGIApp`` class can forward any traffic that is not +Socket.IO to another ASGI application, making it possible to deploy a standard +ASGI web application built with a framework such as FastAPI and the Socket.IO +server as a bundle:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio, other_asgi_app) + +The following example starts the application with Uvicorn:: + + uvicorn --port 5000 module:app + +Uvicorn can also be used through its Gunicorn worker:: + + gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000 + +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. + +Hypercorn, Daphne, and other ASGI servers +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +To use an ASGI web server other than Uvicorn, configure the application for +ASGI as shown above for Uvicorn, then follow the documentation of your chosen +web server to start the application. Aiohttp ~~~~~~~ -`Aiohttp `_ is a framework with support for HTTP -and WebSocket, based on asyncio. Support for this framework is limited to Python -3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use aiohttp -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: +Another option for deploying an asynchronous Socket.IO server is to use the +`Aiohttp `_ web framework and server. Instances +of class ``socketio.AsyncServer`` will automatically use Aiohttp +if the library is installed. To request its use explicitly, the ``async_mode`` +option can be given in the constructor:: sio = socketio.AsyncServer(async_mode='aiohttp') -A server configured for aiohttp must be attached to an existing application:: +A server configured for Aiohttp must be attached to an existing application:: app = web.Application() sio.attach(app) -The aiohttp application can define regular routes that will coexist with the +The Aiohttp application can define regular routes that will coexist with the Socket.IO server. A typical pattern is to add routes that serve a client application and any associated static files. -The aiohttp application is then executed in the usual manner:: +The Aiohttp application is then executed in the usual manner:: if __name__ == '__main__': web.run_app(app) -Tornado -~~~~~~~ +Gevent +~~~~~~ -`Tornado `_ is a web framework with support -for HTTP and WebSocket. Support for this framework requires Python 3.5 and -newer. Only Tornado version 5 and newer are supported, thanks to its tight -integration with asyncio. +When a multi-threaded web server is unable to satisfy the concurrency and +scalability requirements of the application, an option to try is +`Gevent `_. Gevent is a coroutine-based concurrency library +based on greenlets, which are significantly lighter than threads. -Instances of class ``socketio.AsyncServer`` will automatically use tornado -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: +Instances of class ``socketio.Server`` will automatically use Gevent if the +library is installed. To request gevent to be selected explicitly, the +``async_mode`` option can be given in the constructor:: - sio = socketio.AsyncServer(async_mode='tornado') + sio = socketio.Server(async_mode='gevent') -A server configured for tornado must include a request handler for -Socket.IO:: +The Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: - app = tornado.web.Application( - [ - (r"/socketio.io/", socketio.get_tornado_handler(sio)), - ], - # ... other application options - ) + app = socketio.WSGIApp(sio) -The tornado application can define other routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: -The tornado application is then executed in the usual manner:: + sio = socketio.Server(async_mode='gevent') + app = socketio.WSGIApp(sio, other_wsgi_app) - app.listen(port) - tornado.ioloop.IOLoop.current().start() +A server configured for Gevent is deployed as a regular WSGI application +using the provided ``socketio.WSGIApp``:: -Sanic -~~~~~ + from gevent import pywsgi -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. + pywsgi.WSGIServer(('', 8000), app).serve_forever() -Instances of class ``socketio.AsyncServer`` will automatically use Sanic for -asynchronous operations if the framework is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: +Gevent with Gunicorn +!!!!!!!!!!!!!!!!!!!! - sio = socketio.AsyncServer(async_mode='sanic') +An alternative to running the gevent WSGI server as above is to use +`Gunicorn `_ with its Gevent worker. The command to launch the +application under Gunicorn and Gevent is shown below:: -A server configured for aiohttp must be attached to an existing application:: + $ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app - app = Sanic() - sio.attach(app) +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. -The Sanic application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +the Socket.IO server does not require monkey patching, other libraries such as +database or message queue drivers are likely to require it. -The Sanic application is then executed in the usual manner:: +Gevent with uWSGI +!!!!!!!!!!!!!!!!! - if __name__ == '__main__': - app.run() +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. -It has been reported that the CORS support provided by the Sanic extension -`sanic-cors `_ is incomaptible with -this package's own support for this protocol. To disable CORS support in this -package and let Sanic take full control, initialize the server as follows:: +Instances of class ``socketio.Server`` will automatically use this option for +asynchronous operations if both gevent and uWSGI are installed and eventlet is +not installed. To request this asynchronous mode explicitly, the +``async_mode`` option can be given in the constructor:: - sio = socketio.AsyncServer(async_mode='sanic', cors_allowed_origins=[]) + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') -On the Sanic side you will need to enable the `CORS_SUPPORTS_CREDENTIALS` -setting in addition to any other configuration that you use:: +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the ``latency.py`` example on port 5000:: - app.config['CORS_SUPPORTS_CREDENTIALS'] = True + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app -Uvicorn, Daphne, and other ASGI servers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Tornado +~~~~~~~ -The ``socketio.ASGIApp`` class is an ASGI compatible application that can -forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: +Instances of class ``socketio.AsyncServer`` will automatically use +`Tornado `_ if the library is installed. To +request its use explicitly, the ``async_mode`` option can be given in the +constructor:: - sio = socketio.AsyncServer(async_mode='asgi') - app = socketio.ASGIApp(sio) + sio = socketio.AsyncServer(async_mode='tornado') + +A server configured for Tornado must include a request handler for +Socket.IO:: + + app = tornado.web.Application( + [ + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The Tornado application can define other routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. -The application can then be deployed with any ASGI compatible web server. +The Tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() Eventlet ~~~~~~~~ +.. note:: + Eventlet is not in active development anymore, and for that reason the + current recommendation is to not use it for new projects. + `Eventlet `_ is a high performance concurrent networking -library for Python 2 and 3 that uses coroutines, enabling code to be written in -the same style used with the blocking standard library functions. An Socket.IO +library for Python that uses coroutines, enabling code to be written in the +same style used with the blocking standard library functions. An Socket.IO server deployed with eventlet has access to the long-polling and WebSocket transports. @@ -700,12 +952,13 @@ explicitly, the ``async_mode`` option can be given in the constructor:: A server configured for eventlet is deployed as a regular WSGI application using the provided ``socketio.WSGIApp``:: - app = socketio.WSGIApp(sio) import eventlet + + app = socketio.WSGIApp(sio) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) Eventlet with Gunicorn -~~~~~~~~~~~~~~~~~~~~~~ +!!!!!!!!!!!!!!!!!!!!!! An alternative to running the eventlet WSGI server as above is to use `gunicorn `_, a fully featured pure Python web server. The @@ -713,166 +966,166 @@ command to launch the application under gunicorn is shown below:: $ gunicorn -k eventlet -w 1 module:app -Due to limitations in its load balancing algorithm, gunicorn can only be used -with one worker process, so the ``-w`` option cannot be set to a value higher -than 1. A single eventlet worker can handle a large number of concurrent -clients, each handled by a greenlet. +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. Eventlet provides a ``monkey_patch()`` function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. While python-socketio does not require monkey patching, other libraries such as database drivers are likely to require it. -Gevent -~~~~~~ +Sanic +~~~~~ -`Gevent `_ is another asynchronous framework based on -coroutines, very similar to eventlet. An Socket.IO server deployed with -gevent has access to the long-polling transport. If project -`gevent-websocket `_ is -installed, the WebSocket transport is also available. +.. note:: + The Sanic integration has not been updated in a long time. It is currently + recommended that a Sanic application is deployed with the ASGI integration. -Instances of class ``socketio.Server`` will automatically use gevent for -asynchronous operations if the library is installed and eventlet is not -installed. To request gevent to be selected explicitly, the ``async_mode`` -option can be given in the constructor:: +.. _using-a-message-queue: - sio = socketio.Server(async_mode='gevent') - -A server configured for gevent is deployed as a regular WSGI application -using the provided ``socketio.WSGIApp``:: +Using a Message Queue +--------------------- - app = socketio.WSGIApp(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() +When working with distributed applications, it is often necessary to access +the functionality of the Socket.IO from multiple processes. There are two +specific use cases: -If the WebSocket transport is installed, then the server must be started as -follows:: +- Highly available applications may want to use horizontal scaling of the + Socket.IO server to be able to handle very large number of concurrent + clients. +- Applications that use work queues such as + `Celery `_ may need to emit an event to a + client once a background job completes. The most convenient place to carry + out this task is the worker process that handled this job. - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.WSGIApp(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() +As a solution to the above problems, the Socket.IO server can be configured +to connect to a message queue such as `Redis `_ or +`RabbitMQ `_, to communicate with other related +Socket.IO servers or auxiliary workers. -Gevent with Gunicorn -~~~~~~~~~~~~~~~~~~~~ +Redis +~~~~~ -An alternative to running the gevent WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: +To use a Redis message queue, a Python Redis client must be installed:: - $ gunicorn -k gevent -w 1 module:app + # socketio.Server class + pip install redis -Or to include WebSocket:: +The Redis queue is configured through the :class:`socketio.RedisManager` and +:class:`socketio.AsyncRedisManager` classes. These classes connect directly to +the Redis store and use the queue's pub/sub functionality:: - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) -Same as with eventlet, due to limitations in its load balancing algorithm, -gunicorn can only be used with one worker process, so the ``-w`` option cannot -be higher than 1. A single gevent worker can handle a large number of -concurrent clients through the use of greenlets. + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(client_manager=mgr) -Gevent provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. -uWSGI +Kombu ~~~~~ -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: -Instances of class ``socketio.Server`` will automatically use this option for -asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchronous mode explicitly, the -``async_mode`` option can be given in the constructor:: + pip install kombu - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') +To use RabbitMQ or other AMQP protocol compatible queues, that is the only +required dependency. But for other message queues, Kombu may require +additional packages. For example, to use a Redis queue via Kombu, the Python +package for Redis needs to be installed as well:: -A complete explanation of the configuration and usage of the uWSGI server is -beyond the scope of this documentation. The uWSGI server is a fairly complex -package that provides a large and comprehensive set of options. It must be -compiled with WebSocket and SSL support for the WebSocket transport to be -available. As way of an introduction, the following command starts a uWSGI -server for the ``latency.py`` example on port 5000:: + pip install redis - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app +The queue is configured through the :class:`socketio.KombuManager`:: -Standard Threads -~~~~~~~~~~~~~~~~ + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) -While not comparable to eventlet and gevent in terms of performance, -the Socket.IO server can also be configured to work with multi-threaded web -servers that use standard Python threads. This is an ideal setup to use with -development servers such as `Werkzeug `_. Only the -long-polling transport is currently available when using standard threads. +The connection URL passed to the :class:`KombuManager` constructor is passed +directly to Kombu's `Connection object +`_, so +the Kombu documentation should be consulted for information on how to build +the correct URL for a given message queue. -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are not installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. - sio = socketio.Server(async_mode='threading') +Kafka +~~~~~ -A server configured for threading is deployed as a regular web application, -using any WSGI complaint multi-threaded server. The example below deploys an -Socket.IO application combined with a Flask web application, using Flask's -development web server based on Werkzeug:: +`Apache Kafka `_ is supported through the +`kafka-python `_ +package:: - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) + pip install kafka-python - # ... Socket.IO and Flask handler functions ... +Access to Kafka is configured through the :class:`socketio.KafkaManager` +class:: - if __name__ == '__main__': - app.run(threaded=True) + mgr = socketio.KafkaManager('kafka://') + sio = socketio.Server(client_manager=mgr) -When using the threading mode, it is important to ensure that the WSGI server -can handle multiple concurrent requests using threads, since a client can have -up to two outstanding requests at any given time. The Werkzeug server is -single-threaded by default, so the ``threaded=True`` option is required. +Note that Kafka currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. -Note that servers that use worker processes instead of threads, such as -gunicorn, do not support a Socket.IO server configured in threading mode. +AioPika +~~~~~~~ + +A RabbitMQ message queue is supported in asyncio applications through the +`AioPika `_ package:: +You need to install aio_pika with pip:: + + pip install aio_pika -Scalability Notes -~~~~~~~~~~~~~~~~~ +The RabbitMQ queue is configured through the +:class:`socketio.AsyncAioPikaManager` class:: + + mgr = socketio.AsyncAioPikaManager('amqp://') + sio = socketio.AsyncServer(client_manager=mgr) + +Horizontal Scaling +~~~~~~~~~~~~~~~~~~ Socket.IO is a stateful protocol, which makes horizontal scaling more -difficult. To deploy a cluster of Socket.IO processes hosted on one or -multiple servers, the following conditions must be met: +difficult. When deploying a cluster of Socket.IO processes, all processes must +connect to the message queue by passing the ``client_manager`` argument to the +server instance. This enables the workers to communicate and coordinate complex +operations such as broadcasts. + +If the long-polling transport is used, then there are two additional +requirements that must be met: - Each Socket.IO process must be able to handle multiple requests - concurrently. This is required because long-polling clients send two + concurrently. This is needed because long-polling clients send two requests in parallel. Worker processes that can only handle one request at a time are not supported. - The load balancer must be configured to always forward requests from a - client to the same worker process. Load balancers call this *sticky - sessions*, or *session affinity*. -- The worker processes need to communicate with each other to coordinate - complex operations such as broadcasts. This is done through a configured - message queue. See the section on using message queues for details. + client to the same worker process, so that all requests coming from a client + are handled by the same node. Load balancers call this *sticky sessions*, or + *session affinity*. -Cross-Origin Controls ---------------------- +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For security reasons, this server enforces a same-origin policy by default. In -practical terms, this means the following: +To have a process other than a server connect to the queue to emit a message, +the same client manager classes can be used as standalone objects. In this +case, the ``write_only`` argument should be set to ``True`` to disable the +creation of a listening thread, which only makes sense in a server. For +example:: -- If an incoming HTTP or WebSocket request includes the ``Origin`` header, - this header must match the scheme and host of the connection URL. In case - of a mismatch, a 400 status code response is returned and the connection is - rejected. -- No restrictions are imposed on incoming requests that do not include the - ``Origin`` header. + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) -If necessary, the ``cors_allowed_origins`` option can be used to allow other -origins. This argument can be set to a string to set a single allowed origin, or -to a list to allow multiple origins. A special value of ``'*'`` can be used to -instruct the server to allow all origins, but this should be done with care, as -this could make the server vulnerable to Cross-Site Request Forgery (CSRF) -attacks. + # emit an event + external_sio.emit('my event', data={'foo': 'bar'}, room='my room') + +A limitation of the write-only client manager object is that it cannot receive +callbacks when emitting. When the external process needs to receive callbacks, +using a client to connect to the server with read and write support is a better +option than a write-only client manager. diff --git a/examples/README.rst b/examples/README.rst index 8651bba6..82b918e5 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -2,5 +2,5 @@ Socket.IO Examples ================== This directory contains several example Socket.IO applications. Look in the -`server` directory for Socket.IO servers, and in the `client` directory for -Socket.IO clients. \ No newline at end of file +`server` directory for Socket.IO servers, and in the `client` and +`simple-client` directories for Socket.IO clients. diff --git a/examples/client/README.rst b/examples/client/README.rst index d8a17ec3..ce5dab04 100644 --- a/examples/client/README.rst +++ b/examples/client/README.rst @@ -4,12 +4,17 @@ Socket.IO Client Examples This directory contains several example Socket.IO client applications, organized by directory: -threads -------- +sync +---- Examples that use standard Python thread concurrency. -asyncio -------- +async +----- Examples that use Python's `asyncio` package for concurrency. + +javascript +---------- + +Examples that use the JavaScript version of Socket.IO for compatiblity testing. diff --git a/examples/client/asyncio/README.rst b/examples/client/async/README.rst similarity index 57% rename from examples/client/asyncio/README.rst rename to examples/client/async/README.rst index c4ff8e2b..57102139 100644 --- a/examples/client/asyncio/README.rst +++ b/examples/client/async/README.rst @@ -1,8 +1,8 @@ -Socket.IO Asyncio Examples -========================== +Socket.IO Async Client Examples +=============================== This directory contains example Socket.IO clients that work with the -`asyncio` package of the Python standard library. +``asyncio`` package of the Python standard library. latency_client.py ----------------- @@ -14,11 +14,19 @@ for each of these exchanges. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + Running the Examples -------------------- These examples work with the server examples of the same name. First run one -of the `latency.py` versions from the `examples/server/wsgi` directory. On -another terminal, then start the corresponding client:: +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/async/fiddle_client.py b/examples/client/async/fiddle_client.py new file mode 100644 index 00000000..e5aeb6cc --- /dev/null +++ b/examples/client/async/fiddle_client.py @@ -0,0 +1,28 @@ +import asyncio +import socketio + +sio = socketio.AsyncClient() + + +@sio.event +async def connect(): + print('connected to server') + + +@sio.event +async def disconnect(reason): + print('disconnected from server, reason:', reason) + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +async def start_server(): + await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + await sio.wait() + + +if __name__ == '__main__': + asyncio.run(start_server()) diff --git a/examples/client/asyncio/latency_client.py b/examples/client/async/latency_client.py similarity index 85% rename from examples/client/asyncio/latency_client.py rename to examples/client/async/latency_client.py index 79d8f8fa..57be604d 100644 --- a/examples/client/asyncio/latency_client.py +++ b/examples/client/async/latency_client.py @@ -21,11 +21,11 @@ async def connect(): @sio.event async def pong_from_server(): - global start_timer latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') await sio.sleep(1) - await send_ping() + if sio.connected: + await send_ping() async def start_server(): diff --git a/examples/client/javascript/README.md b/examples/client/javascript/README.md new file mode 100644 index 00000000..eaf55e28 --- /dev/null +++ b/examples/client/javascript/README.md @@ -0,0 +1,10 @@ + +# Socket.IO Fiddle + +``` +$ npm install +$ node fiddle-client.js # to run the fiddle example +$ node latency-client.js # to run the latency example +``` + +Optionally, specify a port by supplying the `PORT` env variable. diff --git a/examples/client/javascript/fiddle_client.js b/examples/client/javascript/fiddle_client.js new file mode 100644 index 00000000..9421b55c --- /dev/null +++ b/examples/client/javascript/fiddle_client.js @@ -0,0 +1,16 @@ +const io = require('socket.io-client') +const port = process.env.PORT || 5000; + +const socket = io('http://localhost:' + port, {auth: {token: 'my-token'}}); + +socket.on('connect', () => { + console.log(`connect ${socket.id}`); +}); + +socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); +}); + +socket.on('hello', (a, b, c) => { + console.log(a, b, c); +}); diff --git a/examples/client/javascript/latency_client.js b/examples/client/javascript/latency_client.js new file mode 100644 index 00000000..ef4d0863 --- /dev/null +++ b/examples/client/javascript/latency_client.js @@ -0,0 +1,24 @@ +const io = require('socket.io-client') +const port = process.env.PORT || 5000; + +const socket = io('http://localhost:' + port); +let last; +function send () { + last = new Date(); + socket.emit('ping_from_client'); +} + +socket.on('connect', () => { + console.log(`connect ${socket.id}`); + send(); +}); + +socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); +}); + +socket.on('pong_from_server', () => { + const latency = new Date() - last; + console.log('latency is ' + latency + ' ms'); + setTimeout(send, 1000); +}); diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json new file mode 100644 index 00000000..5a2ec0ca --- /dev/null +++ b/examples/client/javascript/package-lock.json @@ -0,0 +1,1796 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "express": "^4.21.2", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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 + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + }, + "dependencies": { + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + } + } +} diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json new file mode 100644 index 00000000..6bea0d87 --- /dev/null +++ b/examples/client/javascript/package.json @@ -0,0 +1,10 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "express": "^4.21.2", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } +} diff --git a/examples/client/threads/README.rst b/examples/client/sync/README.rst similarity index 59% rename from examples/client/threads/README.rst rename to examples/client/sync/README.rst index 5333375e..efc4d6f9 100644 --- a/examples/client/threads/README.rst +++ b/examples/client/sync/README.rst @@ -1,8 +1,8 @@ -Socket.IO Threading Examples -============================ +Socket.IO Client Examples +========================= This directory contains example Socket.IO clients that work with the -`threading` package of the Python standard library. +Python standard library. latency_client.py ----------------- @@ -14,11 +14,19 @@ for each of these exchanges. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + Running the Examples -------------------- These examples work with the server examples of the same name. First run one -of the `latency.py` versions from the `examples/server/wsgi` directory. On -another terminal, then start the corresponding client:: +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/sync/fiddle_client.py b/examples/client/sync/fiddle_client.py new file mode 100644 index 00000000..71a7a540 --- /dev/null +++ b/examples/client/sync/fiddle_client.py @@ -0,0 +1,23 @@ +import socketio + +sio = socketio.Client() + + +@sio.event +def connect(): + print('connected to server') + + +@sio.event +def disconnect(reason): + print('disconnected from server, reason:', reason) + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +if __name__ == '__main__': + sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + sio.wait() diff --git a/examples/client/threads/latency_client.py b/examples/client/sync/latency_client.py similarity index 73% rename from examples/client/threads/latency_client.py rename to examples/client/sync/latency_client.py index d0a1cf22..94dcec9d 100644 --- a/examples/client/threads/latency_client.py +++ b/examples/client/sync/latency_client.py @@ -1,7 +1,7 @@ import time import socketio -sio = socketio.Client(engineio_logger=True) +sio = socketio.Client(logger=True, engineio_logger=True) start_timer = None @@ -19,11 +19,11 @@ def connect(): @sio.event def pong_from_server(): - global start_timer latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') sio.sleep(1) - send_ping() + if sio.connected: + send_ping() if __name__ == '__main__': diff --git a/examples/server/README.rst b/examples/server/README.rst index 94bacb46..2667f523 100644 --- a/examples/server/README.rst +++ b/examples/server/README.rst @@ -28,3 +28,8 @@ tornado ------- Examples that are compatible with the tornado framework. + +javascript +---------- + +Examples that use the JavaScript version of Socket.IO for compatiblity testing. diff --git a/examples/server/aiohttp/README.rst b/examples/server/aiohttp/README.rst index f1ce6aaf..1b0f4a42 100644 --- a/examples/server/aiohttp/README.rst +++ b/examples/server/aiohttp/README.rst @@ -23,17 +23,27 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py +:: + + $ python fiddle.py + You can then access the application from your web browser at ``http://localhost:8080``. diff --git a/examples/server/aiohttp/app.html b/examples/server/aiohttp/app.html old mode 100755 new mode 100644 index ff896b03..627b9186 --- a/examples/server/aiohttp/app.html +++ b/examples/server/aiohttp/app.html @@ -3,7 +3,7 @@ python-socketio test - + + + + diff --git a/examples/server/aiohttp/fiddle.py b/examples/server/aiohttp/fiddle.py new file mode 100644 index 00000000..64ce330d --- /dev/null +++ b/examples/server/aiohttp/fiddle.py @@ -0,0 +1,31 @@ +from aiohttp import web + +import socketio + +sio = socketio.AsyncServer(async_mode='aiohttp') +app = web.Application() +sio.attach(app) + + +async def index(request): + with open('fiddle.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +app.router.add_static('/static', 'static') +app.router.add_get('/', index) + + +if __name__ == '__main__': + web.run_app(app, port=5000) diff --git a/examples/server/aiohttp/latency.html b/examples/server/aiohttp/latency.html old mode 100755 new mode 100644 index 85338cc8..b3450a61 --- a/examples/server/aiohttp/latency.html +++ b/examples/server/aiohttp/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/asgi/fiddle.py b/examples/server/asgi/fiddle.py new file mode 100644 index 00000000..402a3799 --- /dev/null +++ b/examples/server/asgi/fiddle.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +import uvicorn + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': 'fiddle.html', + '/static': 'static', +}) + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +if __name__ == '__main__': + uvicorn.run(app, host='127.0.0.1', port=5000) diff --git a/examples/server/asgi/latency.html b/examples/server/asgi/latency.html index 85338cc8..b3450a61 100644 --- a/examples/server/asgi/latency.html +++ b/examples/server/asgi/latency.html @@ -11,7 +11,7 @@

(connecting)

- + + + + diff --git a/examples/server/javascript/fiddle_public/main.js b/examples/server/javascript/fiddle_public/main.js new file mode 100644 index 00000000..3070a03e --- /dev/null +++ b/examples/server/javascript/fiddle_public/main.js @@ -0,0 +1,19 @@ +'use strict'; + +(function() { + + const socket = io(); + + socket.on('connect', () => { + console.log(`connect ${socket.id}`); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); + + socket.on('hello', (a, b, c) => { + console.log(a, b, c); + }); + +})(); diff --git a/examples/server/javascript/latency.js b/examples/server/javascript/latency.js new file mode 100644 index 00000000..a2be9946 --- /dev/null +++ b/examples/server/javascript/latency.js @@ -0,0 +1,25 @@ +const express = require('express'); +const { createServer } = require("http"); +const { Server } = require("socket.io"); + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer); + +const port = process.env.PORT || 5000; + +app.use(express.static(__dirname + '/latency_public')); + +io.on('connection', socket => { + console.log(`connect ${socket.id}`); + + socket.on('ping_from_client', () => { + socket.emit('pong_from_server'); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); +}); + +httpServer.listen(port, () => console.log(`server listening on port ${port}`)); diff --git a/examples/server/javascript/latency_public/index.html b/examples/server/javascript/latency_public/index.html new file mode 100644 index 00000000..857ed6a1 --- /dev/null +++ b/examples/server/javascript/latency_public/index.html @@ -0,0 +1,16 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + diff --git a/examples/server/javascript/latency_public/index.js b/examples/server/javascript/latency_public/index.js new file mode 100644 index 00000000..a8975663 --- /dev/null +++ b/examples/server/javascript/latency_public/index.js @@ -0,0 +1,48 @@ +// helper +function $ (id) { return document.getElementById(id); } + +// chart +let smoothie; +let time; + +function render () { + if (smoothie) smoothie.stop(); + $('chart').width = document.body.clientWidth; + smoothie = new SmoothieChart(); + smoothie.streamTo($('chart'), 1000); + time = new TimeSeries(); + smoothie.addTimeSeries(time, { + strokeStyle: 'rgb(255, 0, 0)', + fillStyle: 'rgba(255, 0, 0, 0.4)', + lineWidth: 2 + }); +} + +// socket +const socket = io(); +let last; +function send () { + last = new Date(); + socket.emit('ping_from_client'); + $('transport').innerHTML = socket.io.engine.transport.name; +} + +socket.on('connect', () => { + if ($('chart').getContext) { + render(); + window.onresize = render; + } + send(); +}); + +socket.on('disconnect', () => { + if (smoothie) smoothie.stop(); + $('transport').innerHTML = '(disconnected)'; +}); + +socket.on('pong_from_server', () => { + const latency = new Date() - last; + $('latency').innerHTML = latency + 'ms'; + if (time) time.append(+new Date(), latency); + setTimeout(send, 100); +}); diff --git a/examples/server/javascript/latency_public/style.css b/examples/server/javascript/latency_public/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/server/javascript/latency_public/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json new file mode 100644 index 00000000..4bae767a --- /dev/null +++ b/examples/server/javascript/package-lock.json @@ -0,0 +1,1742 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.21.2", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } + }, + "node_modules/@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "dependencies": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + }, + "peerDependencies": { + "socket.io": ">=3.1.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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 + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + }, + "dependencies": { + "@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "requires": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + } + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + } + } + }, + "engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" + }, + "socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + } + } +} diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json new file mode 100644 index 00000000..1e0e150c --- /dev/null +++ b/examples/server/javascript/package.json @@ -0,0 +1,11 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.21.2", + "smoothie": "1.19.0", + "socket.io": "^4.8.0", + "socket.io-client": "^4.6.1" + } +} diff --git a/examples/server/sanic/README.rst b/examples/server/sanic/README.rst index 53b41604..da2ef2db 100644 --- a/examples/server/sanic/README.rst +++ b/examples/server/sanic/README.rst @@ -26,17 +26,27 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py +:: + + $ python fiddle.py + You can then access the application from your web browser at ``http://localhost:8000``. diff --git a/examples/server/sanic/app.html b/examples/server/sanic/app.html old mode 100755 new mode 100644 index f1e08219..b87b2df1 --- a/examples/server/sanic/app.html +++ b/examples/server/sanic/app.html @@ -1,9 +1,9 @@ - Flask-SocketIO Test + Python-SocketIO Test - + -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/examples/server/sanic/app.py b/examples/server/sanic/app.py old mode 100755 new mode 100644 index b6e8591b..447ddff6 --- a/examples/server/sanic/app.py +++ b/examples/server/sanic/app.py @@ -1,12 +1,10 @@ -import asyncio - from sanic import Sanic from sanic.response import html import socketio sio = socketio.AsyncServer(async_mode='sanic') -app = Sanic() +app = Sanic(__name__) sio.attach(app) @@ -42,14 +40,14 @@ async def my_broadcast_event(sid, message): @sio.event async def join(sid, message): - sio.enter_room(sid, message['room']) + await sio.enter_room(sid, message['room']) await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, room=sid) @sio.event async def leave(sid, message): - sio.leave_room(sid, message['room']) + await sio.leave_room(sid, message['room']) await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, room=sid) @@ -79,8 +77,8 @@ async def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) app.static('/static', './static') diff --git a/examples/server/sanic/fiddle.html b/examples/server/sanic/fiddle.html new file mode 100644 index 00000000..4b58e759 --- /dev/null +++ b/examples/server/sanic/fiddle.html @@ -0,0 +1,11 @@ + + + + + Socket.IO Fiddle + + + + + + diff --git a/examples/server/sanic/fiddle.py b/examples/server/sanic/fiddle.py new file mode 100644 index 00000000..405e6e56 --- /dev/null +++ b/examples/server/sanic/fiddle.py @@ -0,0 +1,32 @@ +from sanic import Sanic +from sanic.response import html + +import socketio + +sio = socketio.AsyncServer(async_mode='sanic') +app = Sanic(__name__) +sio.attach(app) + + +@app.route('/') +def index(request): + with open('fiddle.html') as f: + return html(f.read()) + + +@sio.event +async def connect(sid, environ, auth): + print(f'connected auth={auth} sid={sid}') + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid, reason): + print('disconnected', sid, reason) + + +app.static('/static', './static') + + +if __name__ == '__main__': + app.run() diff --git a/examples/server/sanic/latency.html b/examples/server/sanic/latency.html old mode 100755 new mode 100644 index 85338cc8..b3450a61 --- a/examples/server/sanic/latency.html +++ b/examples/server/sanic/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/tornado/templates/latency.html b/examples/server/tornado/templates/latency.html old mode 100755 new mode 100644 index 85338cc8..b3450a61 --- a/examples/server/tornado/templates/latency.html +++ b/examples/server/tornado/templates/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html old mode 100755 new mode 100644 index db8dc2cc..e37a6cbd --- a/examples/server/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -1,9 +1,9 @@ - Flask-SocketIO Test + Python-SocketIO Test - + -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/examples/server/wsgi/templates/latency.html b/examples/server/wsgi/templates/latency.html old mode 100755 new mode 100644 index c5c13697..e241b5d0 --- a/examples/server/wsgi/templates/latency.html +++ b/examples/server/wsgi/templates/latency.html @@ -11,7 +11,7 @@

(connecting)

- +