diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 527fcb95..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -github: miguelgrinberg -patreon: miguelgrinberg -open_collective: python-socketio -custom: https://paypal.me/miguelgrinberg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5a45f90c..d73d5053 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -23,7 +23,7 @@ Steps to reproduce the 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/logging-and-debugging/) for how to enable 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/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 32257912..968c279f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -17,7 +17,7 @@ A clear and concise description of what you want to happen. 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/logging-and-debugging/) for how to enable 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 index adb0825a..99ad78e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,18 +1,12 @@ name: build -on: - push: - branches: - - main - pull_request: - branches: - - main +on: [push, pull_request, workflow_dispatch] jobs: lint: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - 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 @@ -21,17 +15,13 @@ jobs: name: tests strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.8'] - exclude: - # pypy3 currently fails to run on Windows - - os: windows-latest - python: pypy-3.8 + os: [windows-latest, macos-latest, ubuntu-latest] + python: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python -m pip install --upgrade pip wheel @@ -41,8 +31,8 @@ jobs: name: coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - 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 @@ -50,3 +40,4 @@ jobs: with: files: ./coverage.xml fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 26768c7d..4358e806 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3" sphinx: configuration: docs/conf.py diff --git a/CHANGES.md b/CHANGES.md index 1cda173b..7e97e423 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,151 @@ # python-socketio change log +**Release 5.16.0** - 2025-12-24 + +- Address deprecation warnings ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b235699d9b06564753c570b76055997e9d62a938)) +- Drop Python 3.8 and 3.9 from CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d0728d2f74538762dd551fa9cd0cd1fd5aedfa37)) + +**Release 5.15.1** - 2025-12-16 + +- Restore support multiple arguments via pubsub emits [#1540](https://github.com/miguelgrinberg/python-socketio/issues/1540) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c279f26bb8c9887c4ca99d4d81ad331c4844438c)) + +**Release 5.15.0** - 2025-11-22 + +- Retry initial Redis connection [#1534](https://github.com/miguelgrinberg/python-socketio/issues/1536) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad) [commit #2](https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d)) +- Correctly regenerate RabbitMQ binding after a connection failure [#1516](https://github.com/miguelgrinberg/python-socketio/issues/1516) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c52e93b4a328d98a968bfbdec0cfd598b73ee913)) (thanks **Gritty_dev**!) +- Support `ext_type` in the `MsgPackPacket` class [#1521](https://github.com/miguelgrinberg/python-socketio/issues/1521) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/208925344a48485d2cd56e40eb74266c3bcb5311)) +- Support sending `bytesarray`s when using pub/sub managers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6c9b9974f72e2efdf62407ecab24ee6995448098)) +- Fix typos in documentation [#1520](https://github.com/miguelgrinberg/python-socketio/issues/1520) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db3f1c2a0105c30cb833ddfca8f05fe4320468fd)) (thanks **Lê Nam Khánh**!) +- Improvements to the logging documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b423d0e38eef559b7e81acb7e32059de305f982c)) + +**Release 5.14.3** - 2025-10-29 + +- Support Python's native `ConnectionRefusedError` exception to reject a connection [#1515](https://github.com/miguelgrinberg/python-socketio/issues/1515) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f3b18bde3f16437b223491d4c3e440ea37105fe3)) +- Push binary data to the aiopika client manager [#1514](https://github.com/miguelgrinberg/python-socketio/issues/1514) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/194e1b7f277b5f72e1de78d3f614e7b8b6c788ac)) + +**Release 5.14.2** - 2025-10-15 + +- Restore binary message support in message queue setups [#1509](https://github.com/miguelgrinberg/python-socketio/issues/1509) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3)) +- Fix formatting of client connection error [#1507](https://github.com/miguelgrinberg/python-socketio/issues/1507) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f298c9b54d76ab09ff72935937e1b9575bc45ffd)) +- Add 3.14 and pypy-3.11 CI tasks ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1f4cd3b025c294f25208ec3c05b5f8df6209e403)) +- Improve documentation of the `BaseManager.get_participants()` method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/33722a0d96036f005188b07b8b46a5ef091fe65f)) + +**Release 5.14.1** - 2025-10-02 + +- Restore support for `rediss://` URLs, and add support for `valkeys://` as well ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6e2d0de12bb4e4a99fdfc30bed0706ded620822c)) +- Add support for Redis connections using unix sockets [#1503](https://github.com/miguelgrinberg/python-socketio/issues/1503) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a8deb3a8f3ee51d75c124157efa7fc9289fd592b)) (thanks **Darren Chang**!) + +**Release 5.14.0** - 2025-09-30 + +- Replace pickle with json in message queue communications [#1502](https://github.com/miguelgrinberg/python-socketio/issues/1502) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/53f6be094257ed81476b0e212c8cddd6d06ca39a)) +- Add support for Valkey in the Redis client managers [#1488](https://github.com/miguelgrinberg/python-socketio/issues/1488) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/36a89226a2fb18f876dcba48125a8c51904586ec)) (thanks **phi-friday**!) +- Keep track of which namespaces failed to connect [#1496](https://github.com/miguelgrinberg/python-socketio/issues/1496) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5dc2aea077469ad318e47b28a84845c5efb6bdcf)) +- Fixed transport property of the simple clients to be a string as documented [#1499](https://github.com/miguelgrinberg/python-socketio/issues/1499) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/23556fb3dcb37074020494df40fb4495d47e7efe)) +- SimpleClient.call does not raise `TimeoutError` on timeout [#1501](https://github.com/miguelgrinberg/python-socketio/issues/1501) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a59c6f520059eb095ab4472e5192ce3e486875d9)) (thanks **James Thistlewood**!) +- Wait for client to end background tasks on disconnect [#1500](https://github.com/miguelgrinberg/python-socketio/issues/1500) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f61e0bec3750a83de619db9b779f9d93e449eade)) +- Better error logging for the Redis managers [#1479](https://github.com/miguelgrinberg/python-socketio/issues/1479) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b01b197df1ea5914fdaa2e12758e916ebd60162a)) (thanks **Eugnee**!) +- Channel was not properly initialized in several pubsub client managers [#1476](https://github.com/miguelgrinberg/python-socketio/issues/1476) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/efd1247ed9ed61a61d6840dc2c8c92ab02031afb)) (thanks **Eugnee**!) +- Add message queue deployment recommendations for security ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b3da354ed9eb46c0fb847c628b379ccae475a970)) +- Add missing `async` on session examples for the async server [#1465](https://github.com/miguelgrinberg/python-socketio/issues/1465) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e04003dad0140fb1c6acff328e4215e62fbc58a)) (thanks **Func**!) +- Add SPDX license identifier [#1453](https://github.com/miguelgrinberg/python-socketio/issues/1453) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/81c80cddde06dd9561687c74e5769e25613282d7)) (thanks **Marc Mueller**!) + +**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)) 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 index 7c453c21..780465f4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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. +Python implementation of the `Socket.IO` realtime client and server. Sponsors -------- @@ -40,7 +40,7 @@ JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol Resources --------- -- [Documentation](http://python-socketio.readthedocs.io/en/latest/) +- [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/docs/api.rst b/docs/api.rst index 0c685e6e..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:: 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 e3dcc552..1e23c749 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,197 @@ 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. The value given to these arguments controls logging +behavior: + +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. + +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 +228,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 @@ -68,29 +258,53 @@ or can also be coroutines:: If the server includes arguments with an event, those are passed to the handler function as arguments. -Catch-All Event Handlers ------------------------- +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 catch_all(event, data): - pass + def any_event(event, sid, data): + pass -Asyncio clients can also use a coroutine:: +Asyncio servers can also use a coroutine:: @sio.on('*') - async def catch_all(event, data): - pass + 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. + +Similarly, 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 +The ``connect``, ``connect_error`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @@ -103,8 +317,8 @@ server:: 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 @@ -116,13 +330,26 @@ 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. +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) + +See the The :attr:`socketio.Client.reason` attribute for a list of possible +disconnection reasons. 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:: @@ -138,8 +365,16 @@ 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:: @@ -158,7 +393,7 @@ 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 = request.Session() + http_session = requests.Session() http_session.verify = False sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') @@ -173,7 +408,7 @@ And when using ``asyncio``:: Instead of disabling certificate verification, you can provide a custom certificate authority bundle to verify the certificate against:: - http_session = request.Session() + http_session = requests.Session() http_session.verify = '/path/to/ca.pem' sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') @@ -190,7 +425,7 @@ And for ``asyncio``:: Below you can see how to use a client certificate to authenticate against the server:: - http_session = request.Session() + 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') @@ -206,7 +441,7 @@ And for ``asyncio``:: await sio.connect('https://example.com') Emitting Events ---------------- +~~~~~~~~~~~~~~~ The client can emit an event to the server using the ``emit()`` method:: @@ -216,18 +451,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 @@ -249,7 +485,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 @@ -281,7 +517,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 @@ -291,7 +527,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): @@ -307,7 +543,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): @@ -315,6 +551,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``. @@ -332,7 +573,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:: @@ -344,7 +585,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 @@ -398,7 +639,7 @@ 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:: 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 9cadc6b0..9bb301df 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -101,9 +101,8 @@ Client Features --------------- - Can connect to other Socket.IO servers that are compatible with the - JavaScript Socket.IO 1.x and 2.x releases. Work to support release 3.x is in - progress. -- Compatible with Python 3.6+. + 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 @@ -181,9 +180,8 @@ Server Features --------------- - Can connect to servers running other Socket.IO clients that are compatible - with the JavaScript client versions 1.x and 2.x. Work to support the 3.x - release is in progress. -- Compatible with Python 3.6+. + 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 @@ -191,12 +189,12 @@ Server Features - 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 5a6798c2..e73f14c5 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -19,45 +19,74 @@ 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 `_. - from wsgi import app # a Flask, Django, etc. application - app = socketio.WSGIApp(sio, app) +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 mywebapp import app # a FastAPI or other ASGI application + app = socketio.ASGIApp(sio, app) Serving Static Files --------------------- +~~~~~~~~~~~~~~~~~~~~ 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 @@ -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,34 +211,12 @@ 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. - -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 catch_all(event, sid, data): - pass - -Asyncio servers can also use a coroutine:: - - @sio.on('*') - async def catch_all(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. +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 Event Handlers -------------------------------------- +Connect and Disconnect Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``connect`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @@ -211,29 +226,71 @@ automatically when a client connects or disconnects from the server:: 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. The ``auth`` argument contains any authentication details passed by -the client, or ``None`` if the client did not pass anything. After inspecting -the request, 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`` -:class:`socketio.exceptions.ConnectionRefusedError` can be raised, and all of -its arguments will be sent to the client with the rejection message:: +that was assigned to the client. + +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:: + + from socketio.exceptions import ConnectionRefusedError @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 @@ -241,41 +298,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') + + @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. + +:: -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 currently not supported. + @sio.event + def my_message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) Namespaces ---------- @@ -287,11 +405,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): @@ -305,18 +427,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): @@ -332,7 +460,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): @@ -361,43 +489,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. +Similarly 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 ------------- @@ -466,315 +586,367 @@ 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 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. The value given to these arguments controls logging +behavior: - pip install redis +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. -The queue is configured through the :class:`socketio.KombuManager`:: +Logging can help identify the cause of connection problems, 400 responses, +bad performance and other issues. - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) +Concurrency and Web Server Integration +-------------------------------------- -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. +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:: -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 -~~~~~ +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. -`Apache Kafka `_ is supported through the -`kafka-python `_ -package:: +.. _deployment-strategies: - pip install kafka-python +Deployment Strategies +--------------------- -Access to Kafka is configured through the :class:`socketio.KafkaManager` -class:: +The following sections describe a variety of deployment strategies for +Socket.IO servers. - mgr = socketio.KafkaManager('kafka://') - sio = socketio.Server(client_manager=mgr) +Gunicorn +~~~~~~~~ -Note that Kafka currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. +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:: -AioPika -~~~~~~~ + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio) -A RabbitMQ message queue is supported in asyncio applications through the -`AioPika `_ package:: -You need to install aio_pika with pip:: +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:: - pip install aio_pika + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio, other_wsgi_app) -The RabbitMQ queue is configured through the -:class:`socketio.AsyncAioPikaManager` class:: +The example that follows shows how to start a Socket.IO application using +Gunicorn's threaded worker class:: - mgr = socketio.AsyncAioPikaManager('amqp://') - sio = socketio.AsyncServer(client_manager=mgr) + $ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app -Horizontal Scaling -~~~~~~~~~~~~~~~~~~ +With the above configuration the server will be able to handle close to 100 +concurrent clients. -Socket.IO is a stateful protocol, which makes horizontal scaling more -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. +It is also possible to use more than one worker process, but this has two +additional requirements: -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 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, so that all requests coming from a client - are handled by the same node. Load balancers call this *sticky sessions*, or - *session affinity*. - -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -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:: - - # connect to the redis queue as an external process - external_sio = socketio.RedisManager('redis://', write_only=True) - - # 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. - -Debugging and Troubleshooting ------------------------------ - -To help you debug issues, the server can be configured to output logs to the -terminal:: - - import socketio - - # standard Python - sio = socketio.Server(logger=True, engineio_logger=True) - - # asyncio - sio = socketio.AsyncServer(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, 400 responses, -bad performance and other issues. +- 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. -.. _deployment-strategies: +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. -Deployment Strategies ---------------------- +Note that Gunicorn can also be used alongside ``uvicorn``, ``gevent`` and +``eventlet``. These options are discussed under the appropriate sections below. -The following sections describe a variety of deployment strategies for -Socket.IO servers. +Uvicorn (and other ASGI web servers) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Uvicorn, Daphne, and other ASGI servers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +When working with an asynchronous Socket.IO server, the easiest deployment +strategy is to use an ASGI web server such as +`Uvicorn `_. The ``socketio.ASGIApp`` class is an ASGI compatible application that can -forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: +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 and the Socket.IO server as a bundle:: +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_app) + 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 -The ``ASGIApp`` instance is a fully complaint ASGI instance that can be -deployed with an ASGI compatible web server. +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"/socket.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 -Note: Due to some backward incompatible changes introduced in recent versions -of Sanic, it is currently recommended that a Sanic application is deployed with -the ASGI integration instead. + pywsgi.WSGIServer(('', 8000), app).serve_forever() -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. +Gevent with Gunicorn +!!!!!!!!!!!!!!!!!!!! -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:: +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:: - sio = socketio.AsyncServer(async_mode='sanic') + $ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app -A server configured for aiohttp must be attached to an existing application:: +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. - app = Sanic() - sio.attach(app) +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 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 with uWSGI +!!!!!!!!!!!!!!!!! + +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. -The Sanic application is then executed in the usual manner:: +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:: - if __name__ == '__main__': - app.run() + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') -It has been reported that the CORS support provided by the Sanic extension -`sanic-cors `_ is incompatible 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:: +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:: - sio = socketio.AsyncServer(async_mode='sanic', cors_allowed_origins=[]) + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app -On the Sanic side you will need to enable the `CORS_SUPPORTS_CREDENTIALS` -setting in addition to any other configuration that you use:: +Tornado +~~~~~~~ - app.config['CORS_SUPPORTS_CREDENTIALS'] = True +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='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 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. @@ -787,12 +959,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 @@ -800,154 +973,185 @@ 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') +Using a Message Queue +--------------------- -A server configured for gevent is deployed as a regular WSGI application -using the provided ``socketio.WSGIApp``:: +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: - app = socketio.WSGIApp(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() +- 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. -If the WebSocket transport is installed, then the server must be started as -follows:: +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. - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.WSGIApp(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() +Redis +~~~~~ -Gevent with Gunicorn -~~~~~~~~~~~~~~~~~~~~ +To use a Redis message queue, a Python Redis client must be installed:: -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:: + # socketio.Server class + pip install redis - $ gunicorn -k gevent -w 1 module:app +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:: -Or to include WebSocket:: + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(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. +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. -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. +Kombu +~~~~~ -uWSGI +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: + + pip install kombu + +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:: + + pip install redis + +The queue is configured through the :class:`socketio.KombuManager`:: + + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) + +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. + +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. + +Kafka ~~~~~ -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. +`Apache Kafka `_ is supported through the +`kafka-python `_ +package:: -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 kafka-python - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') +Access to Kafka is configured through the :class:`socketio.KafkaManager` +class:: -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:: + mgr = socketio.KafkaManager('kafka://') + sio = socketio.Server(client_manager=mgr) - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app +Note that Kafka currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. -Standard Threads -~~~~~~~~~~~~~~~~ +AioPika +~~~~~~~ -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 `_. +A RabbitMQ message queue is supported in asyncio applications through the +`AioPika `_ package:: +You need to install aio_pika with pip:: -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: + pip install aio_pika - sio = socketio.Server(async_mode='threading') +The RabbitMQ queue is configured through the +:class:`socketio.AsyncAioPikaManager` class:: -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:: + mgr = socketio.AsyncAioPikaManager('amqp://') + sio = socketio.AsyncServer(client_manager=mgr) - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) +Deploying the Message Queue for Production +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # ... Socket.IO and Flask handler functions ... +For a production deployment there are a few recommendations to keep your +application secure. - if __name__ == '__main__': - app.run() +First of all, the message queue should never be listening on a public network +interface, to ensure that external clients never connect to it. For a single +node deployment, the queue should only listen on `localhost`. For a multi-node +system the use of a private network (VPC), where the communication between +servers can happen privately is highly recommended. -The example that follows shows how to start an Socket.IO application using -Gunicorn's threaded worker class:: +In addition, all message queues support authentication and encryption, which +can strenthen the security of the deployment. Authentication ensures that only +the Socket.IO servers and related processes have access, while encryption +prevents data from being collected by a third-party that is listening on the +network. Access credentials can be included in the connection URLs that are +passed to the client managers. - $ gunicorn -w 1 --threads 100 module:app +Horizontal Scaling +~~~~~~~~~~~~~~~~~~ -With the above configuration the server will be able to handle up to 100 -concurrent clients. +Socket.IO is a stateful protocol, which makes horizontal scaling more +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. -When using standard threads, WebSocket is supported through the -`simple-websocket `_ -package, which must be installed separately. This package provides a -multi-threaded WebSocket server that is compatible with Werkzeug and Gunicorn's -threaded worker. Other multi-threaded web servers are not supported and will -not enable the WebSocket transport. +If the long-polling transport is used, then there are two additional +requirements that must be met: -Cross-Origin Controls ---------------------- +- Each Socket.IO process must be able to handle multiple requests + 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, so that all requests coming from a client + are handled by the same node. Load balancers call this *sticky sessions*, or + *session affinity*. -For security reasons, this server enforces a same-origin policy by default. In -practical terms, this means the following: +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- 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. +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 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. + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) + + # 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 05346572..ce5dab04 100644 --- a/examples/client/README.rst +++ b/examples/client/README.rst @@ -4,13 +4,13 @@ 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. 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/asyncio/fiddle_client.py b/examples/client/async/fiddle_client.py similarity index 81% rename from examples/client/asyncio/fiddle_client.py rename to examples/client/async/fiddle_client.py index 5b43dccd..e5aeb6cc 100644 --- a/examples/client/asyncio/fiddle_client.py +++ b/examples/client/async/fiddle_client.py @@ -10,8 +10,8 @@ async def connect(): @sio.event -async def disconnect(): - print('disconnected from server') +async def disconnect(reason): + print('disconnected from server, reason:', reason) @sio.event diff --git a/examples/client/asyncio/latency_client.py b/examples/client/async/latency_client.py similarity index 88% rename from examples/client/asyncio/latency_client.py rename to examples/client/async/latency_client.py index 00d25392..57be604d 100644 --- a/examples/client/asyncio/latency_client.py +++ b/examples/client/async/latency_client.py @@ -21,9 +21,8 @@ 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) if sio.connected: await send_ping() diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index 27ab1964..fef1fa8f 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -8,9 +8,9 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.18.2", + "express": "^4.22.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" } }, @@ -25,17 +25,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "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": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" + "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", @@ -63,20 +66,20 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "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.4", + "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.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -93,13 +96,28 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -125,9 +143,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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" } @@ -174,6 +192,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -188,34 +219,34 @@ } }, "node_modules/engine.io": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", - "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "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.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "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.0.3", - "ws": "~8.11.0", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, @@ -241,27 +272,27 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "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.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "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.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -273,9 +304,36 @@ } }, "node_modules/engine.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==" + "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.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "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/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/escape-html": { "version": "1.0.3", @@ -291,53 +349,79 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "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/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "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": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -348,6 +432,14 @@ "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", @@ -365,38 +457,63 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "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.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": { - "function-bind": "^1.1.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -404,6 +521,17 @@ "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", @@ -443,6 +571,14 @@ "node": ">= 0.10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -452,9 +588,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "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", @@ -516,9 +655,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -543,9 +685,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "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", @@ -560,11 +702,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "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.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -582,9 +724,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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", @@ -620,9 +762,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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", @@ -648,32 +790,95 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "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/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.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -685,38 +890,61 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "node_modules/socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "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.4.1", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dependencies": { - "ws": "~8.11.0" + "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.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "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.4.0", - "socket.io-parser": "~4.2.1" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { "node": ">=10.0.0" @@ -744,9 +972,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "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" @@ -825,6 +1053,11 @@ "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", @@ -850,15 +1083,15 @@ } }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "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" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -890,17 +1123,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "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": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" + "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", @@ -922,20 +1158,20 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "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.4", + "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.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } @@ -945,13 +1181,22 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "call-bind": { + "call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "content-disposition": { @@ -968,9 +1213,9 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "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", @@ -1004,6 +1249,16 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1015,51 +1270,51 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", - "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "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.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "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.0.3", - "ws": "~8.11.0", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" }, "dependencies": { @@ -1079,9 +1334,27 @@ } }, "engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + "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.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } }, "escape-html": { "version": "1.0.3", @@ -1094,55 +1367,77 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "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==" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } + } } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "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": "~1.0.2", + "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": { @@ -1156,32 +1451,53 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "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.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "requires": { - "function-bind": "^1.1.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "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", @@ -1213,15 +1529,20 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "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.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "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", @@ -1262,9 +1583,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "on-finished": { "version": "2.4.1", @@ -1280,9 +1601,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "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", @@ -1294,11 +1615,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "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.4" + "side-channel": "^1.0.6" } }, "range-parser": { @@ -1307,9 +1628,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "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", @@ -1328,9 +1649,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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", @@ -1355,14 +1676,21 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "setprototypeof": { @@ -1371,13 +1699,47 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "smoothie": { @@ -1386,16 +1748,17 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "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.4.1", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "dependencies": { "debug": { @@ -1414,22 +1777,38 @@ } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "requires": { - "ws": "~8.11.0" + "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.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "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.4.0", - "socket.io-parser": "~4.2.1" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "dependencies": { "debug": { @@ -1448,9 +1827,9 @@ } }, "socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "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" @@ -1490,6 +1869,11 @@ "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", @@ -1506,9 +1890,9 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xmlhttprequest-ssl": { diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index 25edc339..8f1ec830 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -2,9 +2,9 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.18.2", + "express": "^4.22.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "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/threads/fiddle_client.py b/examples/client/sync/fiddle_client.py similarity index 77% rename from examples/client/threads/fiddle_client.py rename to examples/client/sync/fiddle_client.py index 50f5e2aa..71a7a540 100644 --- a/examples/client/threads/fiddle_client.py +++ b/examples/client/sync/fiddle_client.py @@ -9,8 +9,8 @@ def connect(): @sio.event -def disconnect(): - print('disconnected from server') +def disconnect(reason): + print('disconnected from server, reason:', reason) @sio.event diff --git a/examples/client/threads/latency_client.py b/examples/client/sync/latency_client.py similarity index 86% rename from examples/client/threads/latency_client.py rename to examples/client/sync/latency_client.py index 0328d100..94dcec9d 100644 --- a/examples/client/threads/latency_client.py +++ b/examples/client/sync/latency_client.py @@ -19,9 +19,8 @@ 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) if sio.connected: send_ping() diff --git a/examples/server/aiohttp/app.html b/examples/server/aiohttp/app.html index 5cb854df..627b9186 100644 --- 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 index dfde8e10..64ce330d 100644 --- a/examples/server/aiohttp/fiddle.py +++ b/examples/server/aiohttp/fiddle.py @@ -19,8 +19,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) app.router.add_static('/static', 'static') @@ -28,4 +28,4 @@ def disconnect(sid): if __name__ == '__main__': - web.run_app(app) + web.run_app(app, port=5000) diff --git a/examples/server/aiohttp/latency.html b/examples/server/aiohttp/latency.html index 9c885eb9..b3450a61 100644 --- 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 index 6899ed1a..402a3799 100644 --- a/examples/server/asgi/fiddle.py +++ b/examples/server/asgi/fiddle.py @@ -17,8 +17,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) if __name__ == '__main__': diff --git a/examples/server/asgi/latency.html b/examples/server/asgi/latency.html index 9c885eb9..b3450a61 100644 --- a/examples/server/asgi/latency.html +++ b/examples/server/asgi/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/examples/server/sanic/app.py b/examples/server/sanic/app.py index 8e9ab47a..447ddff6 100644 --- a/examples/server/sanic/app.py +++ b/examples/server/sanic/app.py @@ -4,7 +4,7 @@ import socketio sio = socketio.AsyncServer(async_mode='sanic') -app = Sanic(name='sanic_application') +app = Sanic(__name__) sio.attach(app) @@ -40,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) @@ -77,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 index 64dcbd96..4b58e759 100644 --- a/examples/server/sanic/fiddle.html +++ b/examples/server/sanic/fiddle.html @@ -5,7 +5,7 @@ Socket.IO Fiddle - + diff --git a/examples/server/sanic/fiddle.py b/examples/server/sanic/fiddle.py index 8fe4db89..405e6e56 100644 --- a/examples/server/sanic/fiddle.py +++ b/examples/server/sanic/fiddle.py @@ -4,7 +4,7 @@ import socketio sio = socketio.AsyncServer(async_mode='sanic') -app = Sanic() +app = Sanic(__name__) sio.attach(app) @@ -21,8 +21,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) app.static('/static', './static') diff --git a/examples/server/sanic/latency.html b/examples/server/sanic/latency.html index 9c885eb9..b3450a61 100644 --- 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 index 9c885eb9..b3450a61 100644 --- 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 index 5d18f6bb..e37a6cbd 100644 --- 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 index 1124b5a7..e241b5d0 100644 --- a/examples/server/wsgi/templates/latency.html +++ b/examples/server/wsgi/templates/latency.html @@ -11,7 +11,7 @@

(connecting)

- +