From feec96b073e728901fd2de1d8cc2b7f1215ed9c1 Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Sun, 2 Jun 2024 20:37:32 +0300 Subject: [PATCH] Deploy website - based on 8caeeec773610a8d56043fbc5b851775aefce354 --- 404.html | 4 +- assets/js/25003e4f.80401c1a.js | 1 - assets/js/25003e4f.b1536538.js | 1 + assets/js/3a2ce571.c475ab47.js | 1 + assets/js/3a2ce571.d606e2ef.js | 1 - ...c4090.d7e66e5b.js => 3a3c4090.7bb36bf3.js} | 2 +- assets/js/41756ce8.c94731da.js | 1 - assets/js/41756ce8.d88ace41.js | 1 + ...3e6dc.4e2ea8c7.js => 53a3e6dc.98a97af4.js} | 2 +- ...0f1d0.4cb1d67b.js => 5a70f1d0.e607c5bb.js} | 2 +- ...b2670.0a0e2ebf.js => 7a0b2670.8a960fa5.js} | 2 +- assets/js/7f356cfe.5d06fecc.js | 1 + assets/js/b1c31e1e.a52dd055.js | 1 + assets/js/e8c6e505.fb7d0e7e.js | 1 + assets/js/f8e87449.01f2d998.js | 1 - assets/js/f8e87449.8a0967bf.js | 1 + assets/js/main.439c6ff0.js | 2 + ...CENSE.txt => main.439c6ff0.js.LICENSE.txt} | 0 assets/js/main.eb1351b7.js | 2 - assets/js/runtime~main.4aa6c442.js | 1 - assets/js/runtime~main.cc090d83.js | 1 + blog.html | 8 +- .../million-connections-with-centrifugo.html | 4 +- .../16/experimenting-with-quic-transport.html | 4 +- blog/2020/11/12/scaling-websocket.html | 4 +- blog/2021/01/15/centrifuge-intro.html | 4 +- blog/2021/08/31/hello-centrifugo-v3.html | 4 +- blog/2021/10/18/integrating-with-nodejs.html | 4 +- ...with-django-building-chat-application.html | 4 +- .../14/laravel-multi-room-chat-tutorial.html | 4 +- blog/2022/07/19/centrifugo-v4-released.html | 4 +- blog/2022/07/29/101-way-to-subscribe.html | 4 +- .../improving-redis-engine-performance.html | 4 +- blog/2023/03/31/keycloak-sso-centrifugo.html | 4 +- blog/2023/06/29/centrifugo-v5-released.html | 4 +- ...-streaming-to-centrifugo-with-benthos.html | 4 +- .../08/29/using-centrifugo-in-rabbitx.html | 4 +- ...ing-centrifugo-pro-push-notifications.html | 4 +- ...-with-websocket-to-grpc-subscriptions.html | 4 +- ...eal-time-data-compression-experiments.html | 6 +- .../06/03/real-time-document-state-sync.html | 89 ++++++++++++++++++ blog/archive.html | 6 +- blog/atom.xml | 82 +++++++++++++++- blog/feed.json | 17 ++++ blog/rss.xml | 79 +++++++++++++++- blog/tags.html | 6 +- blog/tags/authentication.html | 4 +- blog/tags/benthos.html | 4 +- blog/tags/centrifuge.html | 8 +- blog/tags/centrifugo.html | 8 +- blog/tags/compression.html | 4 +- blog/tags/django.html | 4 +- blog/tags/docsync.html | 24 +++++ blog/tags/go.html | 4 +- blog/tags/grpc.html | 4 +- blog/tags/interview.html | 4 +- blog/tags/keycloak.html | 4 +- blog/tags/laravel.html | 4 +- blog/tags/loki.html | 4 +- blog/tags/php.html | 4 +- blog/tags/pro.html | 4 +- blog/tags/proxy.html | 4 +- blog/tags/push-notifications.html | 4 +- blog/tags/quic.html | 4 +- blog/tags/redis.html | 4 +- blog/tags/release.html | 4 +- blog/tags/sso.html | 4 +- blog/tags/tutorial.html | 4 +- blog/tags/usecase.html | 4 +- blog/tags/websocket.html | 8 +- blog/tags/webtransport.html | 4 +- components/Highlight.html | 4 +- components/logo.html | 4 +- components/logos/Badoo.html | 4 +- components/logos/Grafana.html | 4 +- components/logos/ManyChat.html | 4 +- components/logos/OpenWeb.html | 4 +- docs/3/attributions.html | 4 +- docs/3/ecosystem/centrifuge.html | 4 +- docs/3/ecosystem/integrations.html | 4 +- docs/3/faq.html | 4 +- docs/3/flow_diagrams.html | 4 +- docs/3/getting-started/client_api.html | 4 +- docs/3/getting-started/design.html | 4 +- docs/3/getting-started/highlights.html | 4 +- docs/3/getting-started/installation.html | 4 +- docs/3/getting-started/integration.html | 4 +- docs/3/getting-started/introduction.html | 4 +- docs/3/getting-started/migration_v3.html | 4 +- docs/3/getting-started/quickstart.html | 4 +- docs/3/pro/analytics.html | 4 +- docs/3/pro/db_namespaces.html | 4 +- docs/3/pro/install_and_run.html | 4 +- docs/3/pro/overview.html | 4 +- docs/3/pro/performance.html | 4 +- docs/3/pro/process_stats.html | 4 +- docs/3/pro/singleflight.html | 4 +- docs/3/pro/throttling.html | 4 +- docs/3/pro/token_revocation.html | 4 +- docs/3/pro/tracing.html | 4 +- docs/3/pro/user_block.html | 4 +- docs/3/pro/user_connections.html | 4 +- docs/3/pro/user_status.html | 4 +- docs/3/server/admin_web.html | 4 +- docs/3/server/authentication.html | 4 +- docs/3/server/channels.html | 4 +- docs/3/server/codes.html | 4 +- docs/3/server/configuration.html | 4 +- docs/3/server/console_commands.html | 4 +- docs/3/server/engines.html | 4 +- docs/3/server/history_and_recovery.html | 4 +- docs/3/server/infra_tuning.html | 4 +- docs/3/server/load_balancing.html | 4 +- docs/3/server/monitoring.html | 4 +- docs/3/server/private_channels.html | 4 +- docs/3/server/proxy.html | 4 +- docs/3/server/server_api.html | 4 +- docs/3/server/server_subs.html | 4 +- docs/3/server/tls.html | 4 +- docs/3/transports/client_protocol.html | 4 +- docs/3/transports/client_sdk.html | 4 +- docs/3/transports/overview.html | 4 +- docs/3/transports/sockjs.html | 4 +- docs/3/transports/uni_grpc.html | 4 +- docs/3/transports/uni_http_stream.html | 4 +- docs/3/transports/uni_sse.html | 4 +- docs/3/transports/uni_websocket.html | 4 +- docs/3/transports/websocket.html | 4 +- docs/4/attributions.html | 4 +- docs/4/faq.html | 4 +- docs/4/flow_diagrams.html | 4 +- docs/4/getting-started/client_api.html | 4 +- docs/4/getting-started/community.html | 4 +- docs/4/getting-started/design.html | 4 +- docs/4/getting-started/ecosystem.html | 4 +- docs/4/getting-started/highlights.html | 4 +- docs/4/getting-started/installation.html | 4 +- docs/4/getting-started/integration.html | 4 +- docs/4/getting-started/introduction.html | 4 +- docs/4/getting-started/migration_v4.html | 4 +- docs/4/getting-started/quickstart.html | 4 +- docs/4/pro/analytics.html | 4 +- docs/4/pro/capabilities.html | 4 +- docs/4/pro/cel_expressions.html | 4 +- docs/4/pro/channel_patterns.html | 4 +- docs/4/pro/client_message_batching.html | 4 +- docs/4/pro/connections.html | 4 +- docs/4/pro/install_and_run.html | 4 +- docs/4/pro/overview.html | 4 +- docs/4/pro/performance.html | 4 +- docs/4/pro/process_stats.html | 4 +- docs/4/pro/push_notifications.html | 4 +- docs/4/pro/singleflight.html | 4 +- docs/4/pro/throttling.html | 4 +- docs/4/pro/token_revocation.html | 4 +- docs/4/pro/tracing.html | 4 +- docs/4/pro/user_block.html | 4 +- docs/4/pro/user_status.html | 4 +- docs/4/server/admin_web.html | 4 +- docs/4/server/authentication.html | 4 +- docs/4/server/channel_permissions.html | 4 +- docs/4/server/channel_token_auth.html | 4 +- docs/4/server/channels.html | 4 +- docs/4/server/codes.html | 4 +- docs/4/server/configuration.html | 4 +- docs/4/server/console_commands.html | 4 +- docs/4/server/engines.html | 4 +- docs/4/server/history_and_recovery.html | 4 +- docs/4/server/infra_tuning.html | 4 +- docs/4/server/load_balancing.html | 4 +- docs/4/server/monitoring.html | 4 +- docs/4/server/presence.html | 4 +- docs/4/server/proxy.html | 4 +- docs/4/server/server_api.html | 4 +- docs/4/server/server_subs.html | 4 +- docs/4/server/tls.html | 4 +- docs/4/transports/client_api.html | 4 +- docs/4/transports/client_protocol.html | 4 +- docs/4/transports/client_sdk.html | 4 +- docs/4/transports/http_stream.html | 4 +- docs/4/transports/overview.html | 4 +- docs/4/transports/sockjs.html | 4 +- docs/4/transports/sse.html | 4 +- docs/4/transports/uni_grpc.html | 4 +- docs/4/transports/uni_http_stream.html | 4 +- docs/4/transports/uni_sse.html | 4 +- docs/4/transports/uni_websocket.html | 4 +- docs/4/transports/websocket.html | 4 +- docs/4/transports/webtransport.html | 4 +- docs/attributions.html | 4 +- docs/faq.html | 4 +- docs/flow_diagrams.html | 4 +- docs/getting-started/community.html | 4 +- docs/getting-started/comparisons.html | 4 +- docs/getting-started/design.html | 4 +- docs/getting-started/ecosystem.html | 4 +- docs/getting-started/highlights.html | 4 +- docs/getting-started/installation.html | 4 +- docs/getting-started/integration.html | 4 +- docs/getting-started/introduction.html | 4 +- docs/getting-started/migration_v4.html | 4 +- docs/getting-started/migration_v5.html | 4 +- docs/getting-started/quickstart.html | 4 +- docs/pro/admin_idp_auth.html | 4 +- docs/pro/analytics.html | 4 +- docs/pro/capabilities.html | 4 +- docs/pro/cel_expressions.html | 4 +- docs/pro/channel_cache_empty.html | 4 +- docs/pro/channel_patterns.html | 4 +- docs/pro/channel_state_events.html | 4 +- docs/pro/client_message_batching.html | 4 +- docs/pro/connections.html | 4 +- docs/pro/delta_at_most_once.html | 4 +- docs/pro/distributed_rate_limit.html | 4 +- docs/pro/engine_optimizations.html | 4 +- docs/pro/install_and_run.html | 4 +- docs/pro/observability_enhancements.html | 4 +- docs/pro/overview.html | 4 +- docs/pro/performance.html | 4 +- docs/pro/process_stats.html | 4 +- docs/pro/push_notifications.html | 4 +- docs/pro/rate_limiting.html | 4 +- docs/pro/token_revocation.html | 4 +- docs/pro/tracing.html | 4 +- docs/pro/user_block.html | 4 +- docs/pro/user_status.html | 4 +- docs/server/admin_web.html | 4 +- docs/server/authentication.html | 4 +- docs/server/cache_recovery.html | 4 +- docs/server/channel_permissions.html | 4 +- docs/server/channel_token_auth.html | 4 +- docs/server/channels.html | 4 +- docs/server/codes.html | 4 +- docs/server/configuration.html | 4 +- docs/server/console_commands.html | 4 +- docs/server/consumers.html | 4 +- docs/server/delta_compression.html | 4 +- docs/server/engines.html | 4 +- docs/server/history_and_recovery.html | 4 +- docs/server/infra_tuning.html | 4 +- docs/server/load_balancing.html | 4 +- docs/server/monitoring.html | 4 +- docs/server/observability.html | 4 +- docs/server/presence.html | 4 +- docs/server/proxy.html | 4 +- docs/server/proxy_streams.html | 4 +- docs/server/server_api.html | 4 +- docs/server/server_subs.html | 4 +- docs/server/tls.html | 4 +- docs/transports/client_api.html | 4 +- docs/transports/client_protocol.html | 4 +- docs/transports/client_sdk.html | 4 +- docs/transports/http_stream.html | 4 +- docs/transports/overview.html | 4 +- docs/transports/sockjs.html | 4 +- docs/transports/sse.html | 4 +- docs/transports/uni_client_protocol.html | 4 +- docs/transports/uni_grpc.html | 4 +- docs/transports/uni_http_stream.html | 4 +- docs/transports/uni_sse.html | 4 +- docs/transports/uni_websocket.html | 4 +- docs/transports/websocket.html | 4 +- docs/transports/webtransport.html | 4 +- docs/tutorial/backend.html | 4 +- docs/tutorial/centrifugo.html | 4 +- docs/tutorial/frontend.html | 4 +- docs/tutorial/improvements.html | 4 +- docs/tutorial/intro.html | 4 +- docs/tutorial/layout.html | 4 +- docs/tutorial/outbox_cdc.html | 4 +- docs/tutorial/outro.html | 4 +- docs/tutorial/recovery.html | 4 +- docs/tutorial/reverse_proxy.html | 4 +- docs/tutorial/scale.html | 4 +- docs/tutorial/tips_and_tricks.html | 4 +- img/.DS_Store | Bin 16388 -> 16388 bytes index.html | 4 +- license.html | 4 +- license_exchange_lemon.html | 4 +- search.html | 4 +- sitemap.xml | 2 +- 281 files changed, 823 insertions(+), 533 deletions(-) delete mode 100644 assets/js/25003e4f.80401c1a.js create mode 100644 assets/js/25003e4f.b1536538.js create mode 100644 assets/js/3a2ce571.c475ab47.js delete mode 100644 assets/js/3a2ce571.d606e2ef.js rename assets/js/{3a3c4090.d7e66e5b.js => 3a3c4090.7bb36bf3.js} (70%) delete mode 100644 assets/js/41756ce8.c94731da.js create mode 100644 assets/js/41756ce8.d88ace41.js rename assets/js/{53a3e6dc.4e2ea8c7.js => 53a3e6dc.98a97af4.js} (78%) rename assets/js/{5a70f1d0.4cb1d67b.js => 5a70f1d0.e607c5bb.js} (70%) rename assets/js/{7a0b2670.0a0e2ebf.js => 7a0b2670.8a960fa5.js} (70%) create mode 100644 assets/js/7f356cfe.5d06fecc.js create mode 100644 assets/js/b1c31e1e.a52dd055.js create mode 100644 assets/js/e8c6e505.fb7d0e7e.js delete mode 100644 assets/js/f8e87449.01f2d998.js create mode 100644 assets/js/f8e87449.8a0967bf.js create mode 100644 assets/js/main.439c6ff0.js rename assets/js/{main.eb1351b7.js.LICENSE.txt => main.439c6ff0.js.LICENSE.txt} (100%) delete mode 100644 assets/js/main.eb1351b7.js delete mode 100644 assets/js/runtime~main.4aa6c442.js create mode 100644 assets/js/runtime~main.cc090d83.js create mode 100644 blog/2024/06/03/real-time-document-state-sync.html create mode 100644 blog/tags/docsync.html diff --git a/404.html b/404.html index a8ac3090b..2a6699235 100644 --- a/404.html +++ b/404.html @@ -15,8 +15,8 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

diff --git a/assets/js/25003e4f.80401c1a.js b/assets/js/25003e4f.80401c1a.js deleted file mode 100644 index d36f502b3..000000000 --- a/assets/js/25003e4f.80401c1a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3113],{5556:e=>{e.exports=JSON.parse('{"archive":{"blogPosts":[{"id":"/2024/05/30/real-time-data-compression-experiments","metadata":{"permalink":"/blog/2024/05/30/real-time-data-compression-experiments","editUrl":"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2024-05-30-real-time-data-compression-experiments.md","source":"@site/blog/2024-05-30-real-time-data-compression-experiments.md","title":"Experimenting with real-time data compression by simulating a football match events","description":"This post shows the potential profit of enabling delta compression in channels and demonstrates the reduction of data transfer in various scenarios, including different Centrifugo protocol formats and using WebSocket permessage-deflate compression.","date":"2024-05-30T00:00:00.000Z","tags":[{"label":"centrifugo","permalink":"/blog/tags/centrifugo"},{"label":"websocket","permalink":"/blog/tags/websocket"},{"label":"compression","permalink":"/blog/tags/compression"}],"readingTime":10.435,"hasTruncateMarker":false,"authors":[{"name":"Alexander Emelin","title":"Founder of Centrifugal Labs","imageURL":"/img/alexander_emelin.jpeg"}],"frontMatter":{"title":"Experimenting with real-time data compression by simulating a football match events","tags":["centrifugo","websocket","compression"],"description":"This post shows the potential profit of enabling delta compression in channels and demonstrates the reduction of data transfer in various scenarios, including different Centrifugo protocol formats and using WebSocket permessage-deflate compression.","author":"Alexander Emelin","authorTitle":"Founder of Centrifugal Labs","authorImageURL":"/img/alexander_emelin.jpeg","image":"/img/football_match_compression.jpg","hide_table_of_contents":false},"unlisted":false,"nextItem":{"title":"Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions","permalink":"/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions"}},"content":"\\n\\nOptimizing data transfer over WebSocket connections can significantly reduce bandwidth costs. Compressing data usually leads to memory and CPU resource usage overhead \u2013 but in many cases it worth doing anyway since it positively impacts the final bill from the provider (bandwidth cost reduction overweights resource usage increase).\\n\\nCentrifugo v5.4.0 introduced [delta compression](/docs/server/delta_compression) feature. But before implementing it we wanted a playground which could demonstrate the potential benefit of using delta compression in Centrifugo channels.\\n\\nThis post outlines our approach to estimating the potential profit from implementing delta compression. It demonstrates the reduction in data transfer using once concrete use case across various configurations, including different Centrifugo protocol formats and the additional use of WebSocket permessage-deflate compression. Although these numbers can vary significantly depending on the data, we believe the results are valuable for providing a general understanding of Centrifugo compression options. This information can help Centrifugo users apply these insights to their use cases.\\n\\n## About delta compression\\n\\n![delta frames](/img/delta_abstract.png)\\n\\nFor a good overview of delta compression topic for the real-time messaging applications I suggest starting with a [blog post in Ably engineeiring blog](https://ably.com/blog/message-delta-compression).\\n\\nCentrifugo is very similar to Ably in many aspects (though self-hosted), so everything said in the linked post equally applies to Centrifugo use cases too. Though we have differences in the final implementation, one notable is that we are using [Fossil](https://fossil-scm.org/home/doc/tip/www/delta_format.wiki) delta algorithm in Centrifugo instead of VCDIFF. The reason over VCDIFF was mainly two factors:\\n\\n* availability of several Fossil delta implementations, specifically there are good libraries for Go (see [shadowspore/fossil-delta](https://github.com/shadowspore/fossil-delta)), and for Javascript - [fossil-delta-js](https://github.com/dchest/fossil-delta-js).\\n* the compactness of the algorithm implementation \u2013 under 500 lines of code in JavaScript\\n\\nThe compactness property is nice because there are no OSS Fossil implementations for Java, Dart and Swift \u2013 languages we have SDKs for \u2013 so we may have to implement this algorithm in the future ourselves.\\n\\nHaving said this all, let\'s proceed to the description of experiment we did to understand possible benefits of various compression techniques, and delta compression in particular. \\n\\n## Experiment Overview\\n\\nIn the experiment, we simulated a football match, sending the entire game state over a WebSocket connection upon every match event. Our compression playground looks like this:\\n\\n\\n\\nIt visualizes only the score, but under the hood there are other game changes happen \u2013 will be shown below.\\n\\nWe tested various configurations to evaluate the effectiveness of data compression if different cases. In each setup the same game data was sent over the wire. The data then was captured using [WireShark](https://www.wireshark.org/) with the filter:\\n\\n```\\ntcp.srcport == 8000 && websocket\\n```\\n\\nThis is how WebSocket packets look in Wireshark when applying a filter mentioned above:\\n\\n![wireshark](/img/compression_wireshark.png)\\n\\nBytes captured show the entire overhead from packets in the game channel going from server to client (including TCP/IP overhead).\\n\\nThe source code of the experiment may be found on Github as a [Centrifuge library example](https://github.com/centrifugal/centrifuge/tree/master/_examples/compression_playground). You can run it to inspect the exact WebSocket frames in each scenario.\\n\\nTo give reader a general idea about data, we sent 30 publications with the entire football game state, for example here is a first message in a match (2 teams, 11 players):\\n\\n
\\nClick to see the data\\n

\\n\\n```json\\n{\\n \\"homeTeam\\":{\\n \\"name\\":\\"Real Madrid\\",\\n \\"players\\":[\\n {\\n \\"name\\":\\"John Doe\\"\\n },\\n {\\n \\"name\\":\\"Jane Smith\\"\\n },\\n {\\n \\"name\\":\\"Alex Johnson\\"\\n },\\n {\\n \\"name\\":\\"Chris Lee\\"\\n },\\n {\\n \\"name\\":\\"Pat Kim\\"\\n },\\n {\\n \\"name\\":\\"Sam Morgan\\"\\n },\\n {\\n \\"name\\":\\"Jamie Brown\\"\\n },\\n {\\n \\"name\\":\\"Casey Davis\\"\\n },\\n {\\n \\"name\\":\\"Morgan Garcia\\"\\n },\\n {\\n \\"name\\":\\"Taylor White\\"\\n },\\n {\\n \\"name\\":\\"Jordan Martinez\\"\\n }\\n ]\\n },\\n \\"awayTeam\\":{\\n \\"name\\":\\"Barcelona\\",\\n \\"players\\":[\\n {\\n \\"name\\":\\"Robin Wilson\\"\\n },\\n {\\n \\"name\\":\\"Drew Taylor\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"RED_CARD\\"\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jessie Bailey\\"\\n },\\n {\\n \\"name\\":\\"Casey Flores\\"\\n },\\n {\\n \\"name\\":\\"Jordan Walker\\"\\n },\\n {\\n \\"name\\":\\"Charlie Green\\"\\n },\\n {\\n \\"name\\":\\"Alex Adams\\"\\n },\\n {\\n \\"name\\":\\"Morgan Thompson\\"\\n },\\n {\\n \\"name\\":\\"Taylor Clark\\"\\n },\\n {\\n \\"name\\":\\"Jordan Hernandez\\"\\n },\\n {\\n \\"name\\":\\"Jamie Lewis\\"\\n }\\n ]\\n }\\n}\\n```\\n\\n

\\n
\\n\\nThen we send intermediary states \u2013 someone scores goal, gets yellow card, being subsctituted. And here is the end message in simulation (final scores, final events attached to corresponding players):\\n\\n
\\nClick to see the data\\n

\\n\\n```json\\n{\\n \\"homeTeam\\":{\\n \\"name\\":\\"Real Madrid\\",\\n \\"score\\":3,\\n \\"players\\":[\\n {\\n \\"name\\":\\"John Doe\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":6\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":39\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jane Smith\\"\\n },\\n {\\n \\"name\\":\\"Alex Johnson\\"\\n },\\n {\\n \\"name\\":\\"Chris Lee\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":84\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Pat Kim\\"\\n },\\n {\\n \\"name\\":\\"Sam Morgan\\"\\n },\\n {\\n \\"name\\":\\"Jamie Brown\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":9\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Casey Davis\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":81\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Morgan Garcia\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":15\\n },\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":30\\n },\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":57\\n },\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":62\\n },\\n {\\n \\"type\\":\\"RED_CARD\\",\\n \\"minute\\":66\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Taylor White\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":18\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":42\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":45\\n },\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":69\\n },\\n {\\n \\"type\\":\\"RED_CARD\\",\\n \\"minute\\":72\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jordan Martinez\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":21\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":24\\n }\\n ]\\n }\\n ]\\n },\\n \\"awayTeam\\":{\\n \\"name\\":\\"Barcelona\\",\\n \\"score\\":3,\\n \\"players\\":[\\n {\\n \\"name\\":\\"Robin Wilson\\"\\n },\\n {\\n \\"name\\":\\"Drew Taylor\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"RED_CARD\\"\\n },\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":12\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jessie Bailey\\"\\n },\\n {\\n \\"name\\":\\"Casey Flores\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":78\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jordan Walker\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":33\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Charlie Green\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":51\\n },\\n {\\n \\"type\\":\\"GOAL\\",\\n \\"minute\\":60\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":75\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Alex Adams\\"\\n },\\n {\\n \\"name\\":\\"Morgan Thompson\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":27\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":48\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Taylor Clark\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":3\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":87\\n }\\n ]\\n },\\n {\\n \\"name\\":\\"Jordan Hernandez\\"\\n },\\n {\\n \\"name\\":\\"Jamie Lewis\\",\\n \\"events\\":[\\n {\\n \\"type\\":\\"YELLOW_CARD\\",\\n \\"minute\\":36\\n },\\n {\\n \\"type\\":\\"SUBSTITUTE\\",\\n \\"minute\\":54\\n }\\n ]\\n }\\n ]\\n }\\n}\\n```\\n\\n

\\n
\\n\\nWhen we used Protobuf encoding for game state we serialized the data according to this Protobuf schema:\\n\\n
\\nClick to see the Protobuf schema for the game state\\n

\\n\\n```protobuf\\nsyntax = \\"proto3\\";\\n\\npackage centrifugal.centrifuge.examples.compression_playground;\\n\\noption go_package = \\"./;apppb\\";\\n\\nenum EventType {\\n UNKNOWN = 0; // Default value, should not be used\\n GOAL = 1;\\n YELLOW_CARD = 2;\\n RED_CARD = 3;\\n SUBSTITUTE = 4;\\n}\\n\\nmessage Event {\\n EventType type = 1;\\n int32 minute = 2;\\n}\\n\\nmessage Player {\\n string name = 1;\\n repeated Event events = 2;\\n}\\n\\nmessage Team {\\n string name = 1;\\n int32 score = 2;\\n repeated Player players = 3;\\n}\\n\\nmessage Match {\\n int32 id = 1;\\n Team home_team = 2;\\n Team away_team = 3;\\n}\\n```\\n\\n

\\n
\\n\\n## Results Breakdown\\n\\nBelow are the results of our experiment, comparing different protocols and compression settings:\\n\\n| Protocol | Compression | Delta | Bytes sent | Percentage |\\n|----------------------------|-------------|------------|------------|-----------|\\n| JSON over JSON | No | No | 40251 | 100.0 (base) |\\n| JSON over JSON | Yes | No | 15669 | 38.93 |\\n| JSON over JSON | No | Yes | 6043 | 15.01 |\\n| JSON over JSON | Yes | Yes | 5360 | 13.32 |\\n| -- | -- | -- | -- | -- |\\n| JSON over Protobuf | No | No | 39180 | 97.34 |\\n| JSON over Protobuf | Yes | No | 15542 | 38.61 |\\n| JSON over Protobuf | No | Yes | 4287 | 10.65 |\\n| JSON over Protobuf | Yes | Yes | 4126 | 10.25 |\\n| -- | -- | -- | -- | -- |\\n| Protobuf over Protobuf | No | No | 16562 | 41.15 |\\n| Protobuf over Protobuf | Yes | No | 13115 | 32.58 |\\n| Protobuf over Protobuf | No | Yes | 4382 | 10.89 |\\n| Protobuf over Protobuf | Yes | Yes | 4473 | 11.11 |\\n\\n## Results analysis\\n\\nLet\'s now discuss the results we observed in detail.\\n\\n### JSON over JSON\\n\\nIn this case we are sending JSON data with football match game state over JSON Centrifugal protocol.\\n\\n1. JSON over JSON (No Compression, No Delta)\\nBytes Sent: 40251\\nPercentage: 100.0%\\nAnalysis: This is a baseline scenario, with no compression and no delta, results in the highest amount of data being sent. But very straightforward to implement.\\n\\n2. JSON over JSON (With Compression, No Delta)\\nBytes Sent: 15669\\nPercentage: 38.93%\\nAnalysis: Enabling compression reduces the data size significantly to 38.93% of the original, showcasing the effectiveness of deflate compression. See [how to configure compression](/docs/transports/websocket#websocket_compression) in Centrifugo, note that it comes with CPU and memory overhead which depends on your load profile.\\n\\n3. JSON over JSON (No Compression, With Delta)\\nBytes Sent: 6043\\nPercentage: 15.01%\\nAnalysis: Using delta compression without deflate compression reduces data size to 15.01% for this use case, only changes are being sent after the initial full payload. See how to enable [delta compression in channels](/docs/server/delta_compression) in Centrifugo. The nice thing about using delta compression instead of deflate compression is that deltas require less and more predictable resource overhead. \\n\\n4. JSON over JSON (With Compression and Delta)\\nBytes Sent: 5360\\nPercentage: 13.32%\\nAnalysis: Combining both compression and delta further reduces the data size to 13.32%, achieving the highest efficiency in this category. The benefit is not huge, because we already send small deltas here.\\n\\n### JSON over Protobuf\\n\\nIn this case we are sending JSON data with football match game state over Protobuf Centrifugal protocol.\\n\\n5. JSON over Protobuf (No Compression, No Delta)\\nBytes Sent: 39180\\nPercentage: 97.34%\\nAnalysis: Switching to Protobuf encoding of Centrifugo protocol but still sending JSON data slightly reduces the data size to 97.34% of the JSON over JSON baseline. The benefit here comes from the fact Centrifugo does not introduce a lot of its own protocol overhead \u2013 Protobuf is more compact. But we still send JSON data as Protobuf payloads \u2013 that\'s why it\'s generally comparable with a baseline.\\n\\n6. JSON over Protobuf (With Compression, No Delta)\\nBytes Sent: 15542\\nPercentage: 38.61%\\nAnalysis: Compression with Protobuf encoding brings similar benefits as with JSON, reducing the data size to 38.61%.\\n\\n7. JSON over Protobuf (No Compression, With Delta)\\nBytes Sent: 4287\\nPercentage: 10.65%\\nAnalysis: Delta compression with Protobuf is effective, reducing data to 10.65%. It\'s almost x10 reduction in bandwidth compared to the baseline!\\n\\nI guess at this point you may be curious how delta frames look like in case of JSON protocol. Here is a screenshot:\\n\\n![delta frames](/img/delta_frames.png)\\n\\n8. JSON over Protobuf (With Compression and Delta)\\nBytes Sent: 4126\\nPercentage: 10.25%\\nAnalysis: This combination provides the best results for JSON over Protobuf, reducing data size to 10.25% from the baseline.\\n\\n### Protobuf over Protobuf\\n\\nIn this case we are sending Protobuf binary data with football match game state over Protobuf Centrifugal protocol.\\n\\n9. Protobuf over Protobuf (No Compression, No Delta)\\nBytes Sent: 16562\\nPercentage: 41.15%\\nAnalysis: Using Protobuf for both encoding and transmission **without any compression or delta** reduces data size to 41.15%. So you may get the most efficient setup with nice bandwidth reduction. But the cost is more complex data encoding.\\n\\n10. Protobuf over Protobuf (With Compression, No Delta)\\nBytes Sent: 13115\\nPercentage: 32.58%\\nAnalysis: Compression reduces the data size to 32.58%. Note, that in this case it\'s not very different from JSON case. \\n\\n11. Protobuf over Protobuf (No Compression, With Delta)\\nBytes Sent: 4382\\nPercentage: 10.89%\\nAnalysis: Delta compression is again very effective here, reducing the data size to 10.89%. Again - comparable to JSON case.\\n\\n12. Protobuf over Protobuf (With Compression and Delta)\\nBytes Sent: 4473\\nPercentage: 11.11%\\nAnalysis: Combining both methods results in a data size of 11.11%. Even more than in JSON case. That\'s bacause binary data is not compressed very well with deflate algorithm.\\n\\n## Conclusion\\n\\n* WebSocket permessage-deflate compression significantly reduces the amount of data transferred over WebSocket connections. While it incurs CPU and memory overhead, it may be still worth using from a total cost perspective.\\n\\n* Delta compression makes perfect sense for channels where data changes only slightly between publications. In our experiment, it resulted in a tenfold reduction in bandwidth usage.\\n\\n* Using binary data in combination with the Centrifugo Protobuf protocol provides substantial bandwidth reduction even without deflate or delta compression. However, this comes at the cost of increased data format complexity. An additional benefit of using the Centrifugo Protobuf protocol is its faster marshalling and unmarshalling on the server side compared to the JSON protocol.\\n\\nFor Centrifugo, these results highlighted the potential of implementing delta compression, so we proceeded with it. The benefit depends on the nature of the data being sent \u2013 you can achieve even greater savings if you have larger messages that are very similar to each other."},{"id":"/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions","metadata":{"permalink":"/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions","editUrl":"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2024-03-18-stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.md","source":"@site/blog/2024-03-18-stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.md","title":"Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions","description":"Centrifugo has GRPC subscription streams feature, in this post we show how this feature may simplify a task of delivering data to application UI in real-time. We integrate with Loki, injest log entries and stream logs to the browser based on user-supplied query","date":"2024-03-18T00:00:00.000Z","tags":[{"label":"centrifugo","permalink":"/blog/tags/centrifugo"},{"label":"loki","permalink":"/blog/tags/loki"},{"label":"grpc","permalink":"/blog/tags/grpc"}],"readingTime":7.955,"hasTruncateMarker":true,"authors":[{"name":"Alexander Emelin","title":"Founder of Centrifugal Labs","imageURL":"/img/alexander_emelin.jpeg"}],"frontMatter":{"title":"Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions","tags":["centrifugo","loki","grpc"],"description":"Centrifugo has GRPC subscription streams feature, in this post we show how this feature may simplify a task of delivering data to application UI in real-time. We integrate with Loki, injest log entries and stream logs to the browser based on user-supplied query","author":"Alexander Emelin","authorTitle":"Founder of Centrifugal Labs","authorImageURL":"/img/alexander_emelin.jpeg","image":"/img/centrifugo_loki.jpg","hide_table_of_contents":false},"unlisted":false,"prevItem":{"title":"Experimenting with real-time data compression by simulating a football match events","permalink":"/blog/2024/05/30/real-time-data-compression-experiments"},"nextItem":{"title":"Discovering Centrifugo PRO: push notifications API","permalink":"/blog/2023/10/29/discovering-centrifugo-pro-push-notifications"}},"content":"\\n\\nAs of version 5.1.0, Centrifugo introduces an experimental yet powerful extension that promises to simplify the data delivery process to the browser using GRPC streams. We believe it may help you to solve some practical tasks in minutes. Let\'s dive into how this feature works and how you can leverage it in your applications integrating with [Loki](https://grafana.com/oss/loki/) real-time log streaming capabilities.\\n\\n\x3c!--truncate--\x3e\\n\\n## What Are Proxy Subscription Streams?\\n\\n[Proxy Subscription Streams](/docs/server/proxy_streams) support pushing data directly to Centrifugo client channel subscriptions from your application backend over GRPC streams. This feature is designed to facilitate individual data streams to clients as soon as they subscribe to a channel, acting as a bridge between WebSocket connections from clients and GRPC streams to the backend. It supports both unidirectional (backend to client) and bidirectional (both ways) streams, thereby enhancing flexibility in data streaming.\\n\\n![](/img/on_demand_stream_connections.png)\\n\\nThe design is inspired by [Websocketd](http://websocketd.com/) server \u2013 but while Websocketd transforms data from programs running locally, Centrifugo provides a more generic network interface with GRPC. And all other features of Centrifugo like connection authentication, online presence come as a great bonus.\\n\\nIn the documentation for Proxy Subscription Streams we mentioned streaming logs from Loki as one of the possible use cases. Let\'s expand on the idea and implement the working solution in just 10 minutes.\\n\\n## Demo and source code\\n\\nHere is a demo of what we well get:\\n\\n\\n\\nTake a look at [full source code on Github](https://github.com/centrifugal/examples/tree/master/v5/subscription_streams_loki).\\n\\n## Setting Up Loki\\n\\n[Loki](https://grafana.com/oss/loki/) is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost-effective and easy to operate, making it a perfect candidate for our real-time log streaming example.\\n\\nWe will build the example using Docker Compose, all we have to do for the example is to include Loki image to `docker-compose.yml`: \\n\\n```yaml\\nservices:\\n loki:\\n image: grafana/loki:2.9.5\\n ports:\\n - \\"3100:3100\\"\\n```\\n\\nLoki can ingest logs via various methods, including Promtail, Grafana Agent, Fluentd, and more. For simplicity, we will send logs to Loki ourselves from the Go application.\\n\\nTo send logs to Loki, we can use the HTTP API that Loki provides. This is a straightforward way to push logs directly from an application. The example below demonstrates how to create a simple Go application that generates logs and sends them to Loki using HTTP POST requests.\\n\\nFor this post we will be using Go language to implement the backend part. But it could be any other programming language.\\n\\nFirst, let\'s some code to send a log entries to Loki:\\n\\n```go\\nconst (\\n\\tlokiPushEndpoint = \\"http://loki:3100/loki/api/v1/push\\"\\n)\\n\\ntype lokiPushMessage struct {\\n\\tStreams []lokiStream `json:\\"streams\\"`\\n}\\n\\ntype lokiStream struct {\\n\\tStream map[string]string `json:\\"stream\\"`\\n\\tValues [][]string `json:\\"values\\"`\\n}\\n\\nfunc sendLogMessageToLoki(_ context.Context) error {\\n\\tsources := []string{\\"backend1\\", \\"backend2\\", \\"backend3\\"}\\n\\tsource := sources[rand.Intn(len(sources))]\\n\\tlogMessage := fmt.Sprintf(\\"log from %s source\\", source)\\n\\n\\tpayload := lokiPushMessage{\\n\\t\\tStreams: []lokiStream{\\n\\t\\t\\t{\\n\\t\\t\\t\\tStream: map[string]string{\\n\\t\\t\\t\\t\\t\\"source\\": source,\\n\\t\\t\\t\\t},\\n\\t\\t\\t\\tValues: [][]string{\\n\\t\\t\\t\\t\\t{fmt.Sprintf(\\"%d\\", time.Now().UnixNano()), logMessage},\\n\\t\\t\\t\\t},\\n\\t\\t\\t},\\n\\t\\t},\\n\\t}\\n\\n\\tjsonData, err := json.Marshal(payload)\\n\\tif err != nil {\\n\\t\\treturn err\\n\\t}\\n\\tresp, err := http.Post(\\n\\t\\tlokiPushEndpoint, \\"application/json\\", bytes.NewBuffer(jsonData))\\n\\tif err != nil {\\n\\t\\treturn err\\n\\t}\\n\\tdefer resp.Body.Close()\\n\\n\\tif resp.StatusCode != http.StatusNoContent {\\n\\t\\treturn fmt.Errorf(\\"unexpected status code: %d\\", resp.StatusCode)\\n\\t}\\n\\treturn nil\\n}\\n\\nfunc sendLogsToLoki(ctx context.Context) {\\n\\tfor {\\n\\t\\tselect {\\n\\t\\tcase <-ctx.Done():\\n\\t\\t\\treturn\\n\\t\\tcase <-time.After(200 * time.Millisecond):\\n\\t\\t\\terr := sendLogMessageToLoki(ctx)\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\"error sending log to Loki:\\", err)\\n\\t\\t\\t\\tcontinue\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n}\\n\\nfunc main() {\\n\\tctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)\\n\\tdefer cancel()\\n\\n\\tsendLogsToLoki(ctx)\\n}\\n```\\n\\nThis program defines a `sendLogsToLoki` function that constructs a log entry and sends it to Loki using its HTTP API. It continuously generates log messages every 200 milliseconds.\\n\\nThe `lokiPushMessage` struct is structured to match the JSON payload expected by Loki\'s [`/loki/api/v1/push`](https://grafana.com/docs/loki/latest/reference/api/#push-log-entries-to-loki) endpoint. Each log entry consists of a set of labels (in the Stream map) and log line values, where each value is a two-element array containing the timestamp and the log line. The timestamp is in nanoseconds to match Loki\'s expected format.\\n\\nNote, in the example we randomly set log entry `source` label choosing between `backend1`, `backend2` and `backend3` values.\\n\\nAt this point our program pushes some logs to Loki, now let\'s add Centrifugo to consume them from browser in real-time.\\n\\n## Configuring Centrifugo\\n\\nAdding Centrifugo is also rather straightforward:\\n\\n```yaml\\nservices:\\n centrifugo:\\n image: centrifugo/centrifugo:v5.3.0\\n restart: unless-stopped\\n volumes:\\n - ./centrifugo/config.json:/centrifugo/config.json\\n command: centrifugo -c config.json\\n expose:\\n - 8000\\n```\\n\\nWhere `config.json` is:\\n\\n```json\\n{\\n \\"client_insecure\\": true,\\n \\"allowed_origins\\": [\\"http://localhost:9000\\"],\\n \\"proxy_subscribe_stream_endpoint\\": \\"grpc://backend:12000\\",\\n \\"proxy_subscribe_stream_timeout\\": \\"3s\\",\\n \\"namespaces\\": [\\n {\\n \\"name\\": \\"logs\\",\\n \\"proxy_subscribe_stream\\": true\\n }\\n ]\\n}\\n```\\n\\nNote, we enabled `client_insecure` option here \u2013 this is to keep example short, but in real live you may benefit from Centrifugo authentication: [JWT-based](/docs/server/authentication) or [proxy-based](/docs/server/proxy#connect-proxy).\\n\\n## Writing frontend\\n\\n```html\\n\\n\\n\\n \\n Streaming logs with Centrifugo and Loki\\n\\n\\n
\\n
\\n \\n \\n
\\n
\\n
    \\n
    \\n
    \\n +Centrifugal Blog | Centrifugo @@ -15,10 +15,10 @@ - - + + -
    Skip to main content
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    by Centrifugal team
    We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.
    by Centrifugal team
    Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    by Centrifugal team
    Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second
    +
    Skip to main content
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    by Centrifugal team
    We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.
    by Centrifugal team
    Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    by Centrifugal team
    Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second
    \ No newline at end of file diff --git a/blog/2020/02/10/million-connections-with-centrifugo.html b/blog/2020/02/10/million-connections-with-centrifugo.html index 011c53c64..ff089e4be 100644 --- a/blog/2020/02/10/million-connections-with-centrifugo.html +++ b/blog/2020/02/10/million-connections-with-centrifugo.html @@ -15,8 +15,8 @@ - - + +
    Skip to main content

    Million connections with Centrifugo

    · 4 min read
    Centrifugal team
    Centrifugal team
    Let the Centrifugal force be with you
    diff --git a/blog/2020/10/16/experimenting-with-quic-transport.html b/blog/2020/10/16/experimenting-with-quic-transport.html index a76c630aa..af65cefc2 100644 --- a/blog/2020/10/16/experimenting-with-quic-transport.html +++ b/blog/2020/10/16/experimenting-with-quic-transport.html @@ -15,8 +15,8 @@ - - + +

    Experimenting with QUIC and WebTransport

    · 15 min read
    Alexander Emelin
    Alexander Emelin
    Creator of Centrifugo

    post-cover

    diff --git a/blog/2020/11/12/scaling-websocket.html b/blog/2020/11/12/scaling-websocket.html index 1f1b519e4..6ecf98ef0 100644 --- a/blog/2020/11/12/scaling-websocket.html +++ b/blog/2020/11/12/scaling-websocket.html @@ -15,8 +15,8 @@ - - + +

    Scaling WebSocket in Go and beyond

    · 19 min read
    Alexander Emelin
    Alexander Emelin
    Creator of Centrifugo

    gopher-broker

    diff --git a/blog/2021/01/15/centrifuge-intro.html b/blog/2021/01/15/centrifuge-intro.html index 4eca82fdf..194aad3fd 100644 --- a/blog/2021/01/15/centrifuge-intro.html +++ b/blog/2021/01/15/centrifuge-intro.html @@ -15,8 +15,8 @@ - - + +

    Centrifuge – real-time messaging with Go

    · 23 min read
    Alexander Emelin
    Alexander Emelin
    Creator of Centrifugo

    Centrifuge

    diff --git a/blog/2021/08/31/hello-centrifugo-v3.html b/blog/2021/08/31/hello-centrifugo-v3.html index 38bddde5f..ede10722d 100644 --- a/blog/2021/08/31/hello-centrifugo-v3.html +++ b/blog/2021/08/31/hello-centrifugo-v3.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo v3 released

    · 15 min read
    Centrifugal team
    Centrifugal team
    Let the Centrifugal force be with you

    Centrifuge

    diff --git a/blog/2021/10/18/integrating-with-nodejs.html b/blog/2021/10/18/integrating-with-nodejs.html index f00594c2e..ea28e9cc0 100644 --- a/blog/2021/10/18/integrating-with-nodejs.html +++ b/blog/2021/10/18/integrating-with-nodejs.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo integration with NodeJS tutorial

    · 7 min read
    Alexander Emelin
    Alexander Emelin
    Creator of Centrifugo

    Centrifuge

    diff --git a/blog/2021/11/04/integrating-with-django-building-chat-application.html b/blog/2021/11/04/integrating-with-django-building-chat-application.html index 71255b290..1cb9b81cb 100644 --- a/blog/2021/11/04/integrating-with-django-building-chat-application.html +++ b/blog/2021/11/04/integrating-with-django-building-chat-application.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo integration with Django – building a basic chat application

    · 16 min read
    Alexander Emelin
    Alexander Emelin
    Ex-Pythonista

    Centrifuge

    diff --git a/blog/2021/12/14/laravel-multi-room-chat-tutorial.html b/blog/2021/12/14/laravel-multi-room-chat-tutorial.html index d9c32feef..c1d6df1cd 100644 --- a/blog/2021/12/14/laravel-multi-room-chat-tutorial.html +++ b/blog/2021/12/14/laravel-multi-room-chat-tutorial.html @@ -15,8 +15,8 @@ - - + +

    Building a multi-room chat application with Laravel and Centrifugo

    · 11 min read
    Anton Silischev
    Anton Silischev
    Centrifugo contributor

    Image

    diff --git a/blog/2022/07/19/centrifugo-v4-released.html b/blog/2022/07/19/centrifugo-v4-released.html index 94e00d646..a75eb4924 100644 --- a/blog/2022/07/19/centrifugo-v4-released.html +++ b/blog/2022/07/19/centrifugo-v4-released.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo v4 released – a little revolution

    · 21 min read
    Centrifugal team
    Centrifugal team
    Let the Centrifugal force be with you

    Centrifuge

    diff --git a/blog/2022/07/29/101-way-to-subscribe.html b/blog/2022/07/29/101-way-to-subscribe.html index e799dfcad..73534739b 100644 --- a/blog/2022/07/29/101-way-to-subscribe.html +++ b/blog/2022/07/29/101-way-to-subscribe.html @@ -15,8 +15,8 @@ - - + +

    101 ways to subscribe user on a personal channel in Centrifugo

    · 11 min read
    Alexander Emelin
    Alexander Emelin
    Author of Centrifugo

    Centrifuge

    diff --git a/blog/2022/12/20/improving-redis-engine-performance.html b/blog/2022/12/20/improving-redis-engine-performance.html index 90f5dd217..4e9c88083 100644 --- a/blog/2022/12/20/improving-redis-engine-performance.html +++ b/blog/2022/12/20/improving-redis-engine-performance.html @@ -15,8 +15,8 @@ - - + +

    Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library

    · 29 min read
    Alexander Emelin
    Alexander Emelin
    Author of Centrifugo

    Centrifugo_Redis_Engine_Improvements

    diff --git a/blog/2023/03/31/keycloak-sso-centrifugo.html b/blog/2023/03/31/keycloak-sso-centrifugo.html index 199475b3b..99cbc128a 100644 --- a/blog/2023/03/31/keycloak-sso-centrifugo.html +++ b/blog/2023/03/31/keycloak-sso-centrifugo.html @@ -15,8 +15,8 @@ - - + +

    Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket

    · 5 min read
    Alexander Emelin
    Alexander Emelin
    Author of Centrifugo

    diff --git a/blog/2023/06/29/centrifugo-v5-released.html b/blog/2023/06/29/centrifugo-v5-released.html index a605ce176..1214877b2 100644 --- a/blog/2023/06/29/centrifugo-v5-released.html +++ b/blog/2023/06/29/centrifugo-v5-released.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo v5 released

    · 13 min read
    Centrifugal team
    Centrifugal team
    💻✨🔮✨💻

    In Centrifugo v5 we're phasing out old client protocol support, introducing a more intuitive HTTP API, adjusting token management behaviour in SDKs, improving configuration process, and refactoring the history meta ttl option. As the result you get a cleaner, more user-friendly, and optimized Centrifugo experience. And we have important news about the project - check it out in the end of this post.

    diff --git a/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos.html b/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos.html index 119f36bf8..3d50f84ee 100644 --- a/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos.html +++ b/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos.html @@ -15,8 +15,8 @@ - - + +

    Asynchronous message streaming to Centrifugo with Benthos

    · 8 min read
    Alexander Emelin
    Alexander Emelin
    Author of Centrifugo
    diff --git a/blog/2023/08/29/using-centrifugo-in-rabbitx.html b/blog/2023/08/29/using-centrifugo-in-rabbitx.html index 38361786a..40b8b8807 100644 --- a/blog/2023/08/29/using-centrifugo-in-rabbitx.html +++ b/blog/2023/08/29/using-centrifugo-in-rabbitx.html @@ -15,8 +15,8 @@ - - + +

    Using Centrifugo in RabbitX

    · 4 min read
    Centrifugal + RabbitX
    Centrifugal + RabbitX
    The interview with RabbitX engineering team
    diff --git a/blog/2023/10/29/discovering-centrifugo-pro-push-notifications.html b/blog/2023/10/29/discovering-centrifugo-pro-push-notifications.html index be69e86a4..d9584be92 100644 --- a/blog/2023/10/29/discovering-centrifugo-pro-push-notifications.html +++ b/blog/2023/10/29/discovering-centrifugo-pro-push-notifications.html @@ -15,8 +15,8 @@ - - + +

    Discovering Centrifugo PRO: push notifications API

    · 14 min read
    Alexander Emelin
    Alexander Emelin
    Founder of Centrifugal Labs
    diff --git a/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.html b/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.html index dfbce181e..84ce0110a 100644 --- a/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.html +++ b/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions.html @@ -15,8 +15,8 @@ - - + +

    Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions

    · 8 min read
    Alexander Emelin
    Alexander Emelin
    Founder of Centrifugal Labs
    diff --git a/blog/2024/05/30/real-time-data-compression-experiments.html b/blog/2024/05/30/real-time-data-compression-experiments.html index 16bebf2ab..6f91c6d75 100644 --- a/blog/2024/05/30/real-time-data-compression-experiments.html +++ b/blog/2024/05/30/real-time-data-compression-experiments.html @@ -15,8 +15,8 @@ - - + +

    Experimenting with real-time data compression by simulating a football match events

    · 11 min read
    Alexander Emelin
    Alexander Emelin
    Founder of Centrifugal Labs
    @@ -152,6 +152,6 @@

    Conclusion<

    Using binary data in combination with the Centrifugo Protobuf protocol provides substantial bandwidth reduction even without deflate or delta compression. However, this comes at the cost of increased data format complexity. An additional benefit of using the Centrifugo Protobuf protocol is its faster marshalling and unmarshalling on the server side compared to the JSON protocol.

    -

    For Centrifugo, these results highlighted the potential of implementing delta compression, so we proceeded with it. The benefit depends on the nature of the data being sent – you can achieve even greater savings if you have larger messages that are very similar to each other.

    +

    For Centrifugo, these results highlighted the potential of implementing delta compression, so we proceeded with it. The benefit depends on the nature of the data being sent – you can achieve even greater savings if you have larger messages that are very similar to each other.

    \ No newline at end of file diff --git a/blog/2024/06/03/real-time-document-state-sync.html b/blog/2024/06/03/real-time-document-state-sync.html new file mode 100644 index 000000000..823b22a76 --- /dev/null +++ b/blog/2024/06/03/real-time-document-state-sync.html @@ -0,0 +1,89 @@ + + + + + +Proper real-time document state synchronization within Centrifugal ecosystem | Centrifugo + + + + + + + + + + + + + + + +

    Proper real-time document state synchronization within Centrifugal ecosystem

    · 11 min read
    Alexander Emelin
    Alexander Emelin
    Founder of Centrifugal Labs
    +

    Centrifugo and its main building block Centrifuge library for Go both provide a way for clients to receive a stream of events in channels using Subscription objects. Also, there is an automatic history recovery feature which allows clients catching up with missed publications after the reconnect to the WebSocket server and restore the state of a real-time component. While the continuity in the stream is not broken clients can avoid re-fetching a state from the main application database – which optimizes a scenario when many real-time connections reconnect all within a short time interval (for example, during a load balancer restart) by reducing the excessive load on the application database.

    +

    Usually, our users who use recovery features load the document state from the backend, then establish a real-time Subscription to apply changes to the component state coming from the real-time channel. After that the component stays synchronized, even after network issues – due to Centrifugo recovery feature the document state becomes actual again since client catches up the state from the channel history stream.

    +

    There are several hidden complexities in the process though and things left for users to implement. We want to address those here.

    +

    Complexities in state sync

    +

    Gap in time

    +

    The first edge case comes from the fact that there is a possible gap in time between initial loading of the state from the main app database and real-time subscription establishment. Some messages could be published in between of state loading and real-time subscription establishment. So there is a chance that due to this gap in time the component will live in the inconsistent state until the next application page reload. For many apps this is not critical at all, or due to message rates happens very rarely. But in this post we will look at the possible approach to avoid such a case.

    +

    Or imagine a situation when state is loaded, but real-time subscription is delayed due to some temporary error. This increases a gap in time and a chance to miss an intermediary update.

    +

    Centrifugo channel stream offsets are not binded to the application business models in any way, so it's not possible to initially subscribe to the real-time channel and receive all updates happened since the snapshot of the document loaded from the database. There is a way to solve this though, we will cover it shortly.

    +

    Re-sync upon lost continuity

    +

    Another complexity which is left to the user is the need to react on recovered: false flag provided by the SDK when client can not catch up the state upo re-subscription. This may happen due to channel history retention configuration, or simply because history was lost. In this case our SDKs provide users wasRecovering: true and recovered: false flags, and we suggest re-fetching the document state from the backend in such cases. But while you re-fetch the state you are still receiving real-time updates from the subscription – which leads us to something similar to the problem described above, same race conditions may happen leaving the component in the inconsistent state until reload.

    +

    Late delivery of real-time updates

    +

    One more possible problem to discuss is a late delivery of real-time messages.

    +

    When you want to reliably stream document changes to Centrifugo (without loosing any update, due to temporary network issues for example) and keep the order of changes (to not occasionally apply property addition and deletion is different order on the client side) your best bet is using transactional outbox or CDC approaches. So that changes in the database are made atomic and there is a guarantee that the update will be soon issued to the real-time channel of Centrifuge-based server or Centrifugo. Usually transactional outbox or CDC can also maintain correct order of event processing, thus correct order of publising to the real-time channel.

    +

    But this means that upon loading a real-time component it may receive non-actual real-time updates from the real-time subscription – due to outbox table or CDC processing lag. We need a way for the client side to understand whether the update must be applied to the document state or not. Sometimes it's possible to understand due to the nature of component. Like receiving an update with some identifier which already exists in the object on client side. But what if update contains deletion of some property of object? This will mean that object may rollback to non-actual state, then will receive next real-time updates which will move it to back to the actual state. We want to avoid such modifications leading to temporary state glitches at all. Not all cases allow having idempotent real-time updates.

    +

    Even when you are not using outbox/CDC you can still hit a situation of late real-time message delivery. Let's suppose you publish messages to Centrifugal channel synchronously over server publish API reducing the chance of having a lag from the outbox/CDC processing. But the lag may still present. Because while message travelling towards subscriber through Centrifugo, subscriber can load a more freshy initial state from the main database and subscribe to the real-time channel.

    +

    Core principles of solution

    +

    In this post we will write a RealTimeDocument JavaScript class designed to synchronize a document state. This class handles initial data loading, real-time updates, and state re-synchronization when required. It should solve the problems described above.

    +

    The good thing is that this helper class is compact enough to be implemented in any programming language, so you can apply it (or the required part of it) for other languages where we already have real-time SDKs.

    +

    We will build the helper on top of several core principles:

    +
      +
    • The document has an incremental version which is managed atomically and transactionally on the backend.
    • +
    • Initial state loading returns document state together with the current version, loading happens with at least read committed transactional isolation level (default in many databases, ex. PostgreSQL)
    • +
    • All real-time updates published to the document channel have the version attached, and updates are published to the channel in the correct version order.
    • +
    +

    We already discussed the approach in our Grand tutorial – but now want to generalize it as a re-usable pattern.

    +

    After writing a RealTimeDocument wrapper we will apply it to a simple example of synchronizing counter increments across multiple devices reliably to demonstrate it works. Eventually we get best from two worlds – leveraging Centrifugo publication cache to avoid excessive load on the backend upon massive reconnect and proper document state in all scenarios.

    +

    Top-level API of RealTimeDocument

    +
    const subscription = centrifuge.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription, // Wraps Subscription.
    load: async (): Promise<{ document: any; version: number }> => {
    // Must load the actual document state and version from the database.
    // Ex. return { document: result.document, version: result.version };
    },
    applyUpdate: (currentDocument: any, update: any): any => {
    // Must apply update to the document.
    // currentDocument.value += update.increment;
    // return currentDocument;
    },
    compareVersion: (currentVersion: number, update: any): number | null => {
    // Must compare versions in real-time publication and current doc version.
    // const newVersion = publication.data.version;
    // return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document: any) => {
    // Will be called once the document is loaded for the first time and every time
    // the document state is updated. This is where application may render things
    // based on the document data.
    }
    });

    realTimeDocument.startSync();
    +

    Implementing solution

    +

    To address the gap between state load and real-time subscription establishment the obvious solution which is possible with Centrifugal stack is to make the real-time subscription first, and only after that load the state from the backend. This eliminates the possibility to miss messages. But until the state is loaded we need to buffer real-time publications and then apply them to the loaded state.

    +

    Here is where the concept of having incremental document version helps – we can collect messages in the buffer, and then apply only those with version greater than current document version. So that the object will have the correct state after the initial load.

    +

    Here is how we can process real-time publications:

    +
    this.#subscription.on('publication', (ctx) => {
    if (!this.#isLoaded) {
    // Buffer messages until initial state is loaded.
    this.#messageBuffer.push(ctx);
    return;
    }
    // Process new messages immediately if initial state is already loaded.
    const newVersion = this.#compareVersion(ctx.data, this.#version);
    if (newVersion === null) {
    // Skip real-time publication, non actual version.
    return;
    }
    this.#document = this.#applyUpdate(this.#document, ctx.data);
    this.#version = newVersion;
    this.#onChange(this.#document);
    }
    +

    And we also need to handle subscribed event properly and load the initial document state from the backend:

    +
    this.#subscription.on('subscribed', (ctx) => {
    if (ctx.wasRecovering) {
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    } else {
    // Load data for the first time.
    this.#loadDocumentApplyBuffered();
    }
    })
    +

    For the initial load this.#loadDocumentApplyBuffered() will be called. Here is how it may look like:

    +
    async #loadDocumentApplyBuffered() {
    try {
    const result = await this.#load();
    this.#document = result.document;
    this.#version = result.version;
    this.#isLoaded = true;
    this.#processBufferedMessages();
    } catch (error) {
    // Retry the loading, in the final snippet it's implemented
    // and uses exponential backoff for the retry process.
    }
    }
    +

    After loading the state we prosess buffered real-time publications inside #processBufferedMessages method:

    +
    #processBufferedMessages() {
    this.#messageBuffer.forEach((msg) => {
    const newVersion = this.#compareVersion(msg, this.#version);
    if (newVersion) { // Otherwise, skip buffered publication.
    this.#document = this.#applyUpdate(this.#document, msg.data);
    this.#version = newVersion;
    }
    });
    // Clear the buffer after processing.
    this.#messageBuffer = [];
    // Only call onChange with final document state.
    this.#onChange(this.#document);
    }
    +

    This way the initial state is loaded correctly. Note also, that version comparisons also help with handling late delivered real-time updates – we now simply skip them inside on('publication') callback.

    +

    Let's go back and look how to manage stream continuity loss:

    +
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    +

    In this case we call #reSync method:

    +
    #reSync() {
    this.#isLoaded = false; // Reset the flag to collect new messages to the buffer.
    this.#messageBuffer = [];
    this.#loadDocumentApplyBuffered();
    }
    +

    It basically clears up the class state and calls #loadDocumentApplyBuffered again – repeating the initial sync procedure.

    +

    That's it. Here is a full code for the RealTimeDocument class. Note, it also contains backoff implementation to handle possible error while loading the document state from the backend endpoint.

    +

    Let's apply it

    +

    I've made a POC with Centrifuge library to make sure this works.

    +

    In that example I tried to apply RealTimeDocument class to synchronize state of the counter. Periodically timer is incremented on a random value in range [0,9] on the backend and these increments are published to the real-time channel. Note, I could simply publish counter value in every publication over WebSocket – but intentionally decided to send counter increments instead. To make sure nothing is lost during state synchronization so counter value is always correct on the client side.

    +

    Here is a demo:

    +
    +

    Let's look at how RealTimeDocument class was used in the example:

    +
    const counterContainer = document.getElementById("counter");

    const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});
    const subscription = client.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription,
    load: async () => {
    const response = await fetch('/api/counter');
    const result = await response.json();
    return { document: result.value, version: result.version };
    },
    applyUpdate: (document, update) => {
    document += update.increment
    return document
    },
    compareVersion: (currentVersion, update) => {
    const newVersion = update.version;
    return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document) => {
    counterContainer.textContent = document;
    },
    debug: true,
    });
    client.connect();

    // Note – we can call sync even before connect.
    realTimeDocument.startSync();
    +

    Things to observe:

    +
      +
    • We return {"version":4823,"value":21656} from /api/counter
    • +
    • Send {"version":4824,"increment":9} over real-time channel
    • +
    • Counter updated every 250 milliseconds, history size is 20, retention 10 seconds
    • +
    • Upon going offline for a short period we see that /api/counter endpoint not called at all - state fully cought up from Centrifugo history stream
    • +
    • Upon going offline for a longer period Centrifugo was not able to recover the state, so we re-fetched data from scratch and attached to the stream again.
    • +
    +

    Conclusion

    +

    In this post, we walked through a practical implementation of a RealTimeDocument class using Centrifugal stack for the real-time state synchronization to achieve proper eventually consistent state of the document when using real-time updates. We mentioned possible gotchas when trying to sync the state in real-time and described a generic solution to it.

    +

    You don't need to always follow the solution here. As I mentioned it's possible that your app does not require handling all these edge cases, or they could be handled in alternative ways – this heavily depends on your app business logic.

    +

    Note, that with some changes you can make RealTimeDocument class to behave properly and support graceful degradation behaviour. If there are issues with real-time subscription you can still load the document and display it, and then re-sync the state as soon as a real-time system becomes available (successful subscription).

    + + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index 8290c1761..1dd2bc0b7 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -15,10 +15,10 @@ - - + + - +
    \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml index 632c112ac..e5733b78b 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -2,11 +2,91 @@ https://centrifugal.dev/blog - <updated>2024-05-30T00:00:00.000Z</updated> + <updated>2024-06-03T00:00:00.000Z</updated> <generator>https://github.com/jpmonette/feed</generator> <link rel="alternate" href="https://centrifugal.dev/blog"/> <icon>https://centrifugal.dev/img/favicon.png</icon> <rights>Centrifugal Labs LTD</rights> + <entry> + <title type="html"><![CDATA[Proper real-time document state synchronization within Centrifugal ecosystem]]> + https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync + + 2024-06-03T00:00:00.000Z + + +

    Centrifugo and its main building block Centrifuge library for Go both provide a way for clients to receive a stream of events in channels using Subscription objects. Also, there is an automatic history recovery feature which allows clients catching up with missed publications after the reconnect to the WebSocket server and restore the state of a real-time component. While the continuity in the stream is not broken clients can avoid re-fetching a state from the main application database – which optimizes a scenario when many real-time connections reconnect all within a short time interval (for example, during a load balancer restart) by reducing the excessive load on the application database.

    +

    Usually, our users who use recovery features load the document state from the backend, then establish a real-time Subscription to apply changes to the component state coming from the real-time channel. After that the component stays synchronized, even after network issues – due to Centrifugo recovery feature the document state becomes actual again since client catches up the state from the channel history stream.

    +

    There are several hidden complexities in the process though and things left for users to implement. We want to address those here.

    +

    Complexities in state sync

    +

    Gap in time

    +

    The first edge case comes from the fact that there is a possible gap in time between initial loading of the state from the main app database and real-time subscription establishment. Some messages could be published in between of state loading and real-time subscription establishment. So there is a chance that due to this gap in time the component will live in the inconsistent state until the next application page reload. For many apps this is not critical at all, or due to message rates happens very rarely. But in this post we will look at the possible approach to avoid such a case.

    +

    Or imagine a situation when state is loaded, but real-time subscription is delayed due to some temporary error. This increases a gap in time and a chance to miss an intermediary update.

    +

    Centrifugo channel stream offsets are not binded to the application business models in any way, so it's not possible to initially subscribe to the real-time channel and receive all updates happened since the snapshot of the document loaded from the database. There is a way to solve this though, we will cover it shortly.

    +

    Re-sync upon lost continuity

    +

    Another complexity which is left to the user is the need to react on recovered: false flag provided by the SDK when client can not catch up the state upo re-subscription. This may happen due to channel history retention configuration, or simply because history was lost. In this case our SDKs provide users wasRecovering: true and recovered: false flags, and we suggest re-fetching the document state from the backend in such cases. But while you re-fetch the state you are still receiving real-time updates from the subscription – which leads us to something similar to the problem described above, same race conditions may happen leaving the component in the inconsistent state until reload.

    +

    Late delivery of real-time updates

    +

    One more possible problem to discuss is a late delivery of real-time messages.

    +

    When you want to reliably stream document changes to Centrifugo (without loosing any update, due to temporary network issues for example) and keep the order of changes (to not occasionally apply property addition and deletion is different order on the client side) your best bet is using transactional outbox or CDC approaches. So that changes in the database are made atomic and there is a guarantee that the update will be soon issued to the real-time channel of Centrifuge-based server or Centrifugo. Usually transactional outbox or CDC can also maintain correct order of event processing, thus correct order of publising to the real-time channel.

    +

    But this means that upon loading a real-time component it may receive non-actual real-time updates from the real-time subscription – due to outbox table or CDC processing lag. We need a way for the client side to understand whether the update must be applied to the document state or not. Sometimes it's possible to understand due to the nature of component. Like receiving an update with some identifier which already exists in the object on client side. But what if update contains deletion of some property of object? This will mean that object may rollback to non-actual state, then will receive next real-time updates which will move it to back to the actual state. We want to avoid such modifications leading to temporary state glitches at all. Not all cases allow having idempotent real-time updates.

    +

    Even when you are not using outbox/CDC you can still hit a situation of late real-time message delivery. Let's suppose you publish messages to Centrifugal channel synchronously over server publish API reducing the chance of having a lag from the outbox/CDC processing. But the lag may still present. Because while message travelling towards subscriber through Centrifugo, subscriber can load a more freshy initial state from the main database and subscribe to the real-time channel.

    +

    Core principles of solution

    +

    In this post we will write a RealTimeDocument JavaScript class designed to synchronize a document state. This class handles initial data loading, real-time updates, and state re-synchronization when required. It should solve the problems described above.

    +

    The good thing is that this helper class is compact enough to be implemented in any programming language, so you can apply it (or the required part of it) for other languages where we already have real-time SDKs.

    +

    We will build the helper on top of several core principles:

    +
      +
    • The document has an incremental version which is managed atomically and transactionally on the backend.
    • +
    • Initial state loading returns document state together with the current version, loading happens with at least read committed transactional isolation level (default in many databases, ex. PostgreSQL)
    • +
    • All real-time updates published to the document channel have the version attached, and updates are published to the channel in the correct version order.
    • +
    +

    We already discussed the approach in our Grand tutorial – but now want to generalize it as a re-usable pattern.

    +

    After writing a RealTimeDocument wrapper we will apply it to a simple example of synchronizing counter increments across multiple devices reliably to demonstrate it works. Eventually we get best from two worlds – leveraging Centrifugo publication cache to avoid excessive load on the backend upon massive reconnect and proper document state in all scenarios.

    +

    Top-level API of RealTimeDocument

    +
    const subscription = centrifuge.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription, // Wraps Subscription.
    load: async (): Promise<{ document: any; version: number }> => {
    // Must load the actual document state and version from the database.
    // Ex. return { document: result.document, version: result.version };
    },
    applyUpdate: (currentDocument: any, update: any): any => {
    // Must apply update to the document.
    // currentDocument.value += update.increment;
    // return currentDocument;
    },
    compareVersion: (currentVersion: number, update: any): number | null => {
    // Must compare versions in real-time publication and current doc version.
    // const newVersion = publication.data.version;
    // return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document: any) => {
    // Will be called once the document is loaded for the first time and every time
    // the document state is updated. This is where application may render things
    // based on the document data.
    }
    });

    realTimeDocument.startSync();
    +

    Implementing solution

    +

    To address the gap between state load and real-time subscription establishment the obvious solution which is possible with Centrifugal stack is to make the real-time subscription first, and only after that load the state from the backend. This eliminates the possibility to miss messages. But until the state is loaded we need to buffer real-time publications and then apply them to the loaded state.

    +

    Here is where the concept of having incremental document version helps – we can collect messages in the buffer, and then apply only those with version greater than current document version. So that the object will have the correct state after the initial load.

    +

    Here is how we can process real-time publications:

    +
    this.#subscription.on('publication', (ctx) => {
    if (!this.#isLoaded) {
    // Buffer messages until initial state is loaded.
    this.#messageBuffer.push(ctx);
    return;
    }
    // Process new messages immediately if initial state is already loaded.
    const newVersion = this.#compareVersion(ctx.data, this.#version);
    if (newVersion === null) {
    // Skip real-time publication, non actual version.
    return;
    }
    this.#document = this.#applyUpdate(this.#document, ctx.data);
    this.#version = newVersion;
    this.#onChange(this.#document);
    }
    +

    And we also need to handle subscribed event properly and load the initial document state from the backend:

    +
    this.#subscription.on('subscribed', (ctx) => {
    if (ctx.wasRecovering) {
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    } else {
    // Load data for the first time.
    this.#loadDocumentApplyBuffered();
    }
    })
    +

    For the initial load this.#loadDocumentApplyBuffered() will be called. Here is how it may look like:

    +
    async #loadDocumentApplyBuffered() {
    try {
    const result = await this.#load();
    this.#document = result.document;
    this.#version = result.version;
    this.#isLoaded = true;
    this.#processBufferedMessages();
    } catch (error) {
    // Retry the loading, in the final snippet it's implemented
    // and uses exponential backoff for the retry process.
    }
    }
    +

    After loading the state we prosess buffered real-time publications inside #processBufferedMessages method:

    +
    #processBufferedMessages() {
    this.#messageBuffer.forEach((msg) => {
    const newVersion = this.#compareVersion(msg, this.#version);
    if (newVersion) { // Otherwise, skip buffered publication.
    this.#document = this.#applyUpdate(this.#document, msg.data);
    this.#version = newVersion;
    }
    });
    // Clear the buffer after processing.
    this.#messageBuffer = [];
    // Only call onChange with final document state.
    this.#onChange(this.#document);
    }
    +

    This way the initial state is loaded correctly. Note also, that version comparisons also help with handling late delivered real-time updates – we now simply skip them inside on('publication') callback.

    +

    Let's go back and look how to manage stream continuity loss:

    +
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    +

    In this case we call #reSync method:

    +
    #reSync() {
    this.#isLoaded = false; // Reset the flag to collect new messages to the buffer.
    this.#messageBuffer = [];
    this.#loadDocumentApplyBuffered();
    }
    +

    It basically clears up the class state and calls #loadDocumentApplyBuffered again – repeating the initial sync procedure.

    +

    That's it. Here is a full code for the RealTimeDocument class. Note, it also contains backoff implementation to handle possible error while loading the document state from the backend endpoint.

    +

    Let's apply it

    +

    I've made a POC with Centrifuge library to make sure this works.

    +

    In that example I tried to apply RealTimeDocument class to synchronize state of the counter. Periodically timer is incremented on a random value in range [0,9] on the backend and these increments are published to the real-time channel. Note, I could simply publish counter value in every publication over WebSocket – but intentionally decided to send counter increments instead. To make sure nothing is lost during state synchronization so counter value is always correct on the client side.

    +

    Here is a demo:

    +
    +

    Let's look at how RealTimeDocument class was used in the example:

    +
    const counterContainer = document.getElementById("counter");

    const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});
    const subscription = client.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription,
    load: async () => {
    const response = await fetch('/api/counter');
    const result = await response.json();
    return { document: result.value, version: result.version };
    },
    applyUpdate: (document, update) => {
    document += update.increment
    return document
    },
    compareVersion: (currentVersion, update) => {
    const newVersion = update.version;
    return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document) => {
    counterContainer.textContent = document;
    },
    debug: true,
    });
    client.connect();

    // Note – we can call sync even before connect.
    realTimeDocument.startSync();
    +

    Things to observe:

    +
      +
    • We return {"version":4823,"value":21656} from /api/counter
    • +
    • Send {"version":4824,"increment":9} over real-time channel
    • +
    • Counter updated every 250 milliseconds, history size is 20, retention 10 seconds
    • +
    • Upon going offline for a short period we see that /api/counter endpoint not called at all - state fully cought up from Centrifugo history stream
    • +
    • Upon going offline for a longer period Centrifugo was not able to recover the state, so we re-fetched data from scratch and attached to the stream again.
    • +
    +

    Conclusion

    +

    In this post, we walked through a practical implementation of a RealTimeDocument class using Centrifugal stack for the real-time state synchronization to achieve proper eventually consistent state of the document when using real-time updates. We mentioned possible gotchas when trying to sync the state in real-time and described a generic solution to it.

    +

    You don't need to always follow the solution here. As I mentioned it's possible that your app does not require handling all these edge cases, or they could be handled in alternative ways – this heavily depends on your app business logic.

    +

    Note, that with some changes you can make RealTimeDocument class to behave properly and support graceful degradation behaviour. If there are issues with real-time subscription you can still load the document and display it, and then re-sync the state as soon as a real-time system becomes available (successful subscription).

    ]]>
    + + Alexander Emelin + + + + + + <![CDATA[Experimenting with real-time data compression by simulating a football match events]]> https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments diff --git a/blog/feed.json b/blog/feed.json index 2e7c7aceb..0402324e8 100644 --- a/blog/feed.json +++ b/blog/feed.json @@ -3,6 +3,23 @@ "title": "", "home_page_url": "https://centrifugal.dev/blog", "items": [ + { + "id": "https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync", + "content_html": "\n

    Centrifugo and its main building block Centrifuge library for Go both provide a way for clients to receive a stream of events in channels using Subscription objects. Also, there is an automatic history recovery feature which allows clients catching up with missed publications after the reconnect to the WebSocket server and restore the state of a real-time component. While the continuity in the stream is not broken clients can avoid re-fetching a state from the main application database – which optimizes a scenario when many real-time connections reconnect all within a short time interval (for example, during a load balancer restart) by reducing the excessive load on the application database.

    \n

    Usually, our users who use recovery features load the document state from the backend, then establish a real-time Subscription to apply changes to the component state coming from the real-time channel. After that the component stays synchronized, even after network issues – due to Centrifugo recovery feature the document state becomes actual again since client catches up the state from the channel history stream.

    \n

    There are several hidden complexities in the process though and things left for users to implement. We want to address those here.

    \n

    Complexities in state sync

    \n

    Gap in time

    \n

    The first edge case comes from the fact that there is a possible gap in time between initial loading of the state from the main app database and real-time subscription establishment. Some messages could be published in between of state loading and real-time subscription establishment. So there is a chance that due to this gap in time the component will live in the inconsistent state until the next application page reload. For many apps this is not critical at all, or due to message rates happens very rarely. But in this post we will look at the possible approach to avoid such a case.

    \n

    Or imagine a situation when state is loaded, but real-time subscription is delayed due to some temporary error. This increases a gap in time and a chance to miss an intermediary update.

    \n

    Centrifugo channel stream offsets are not binded to the application business models in any way, so it's not possible to initially subscribe to the real-time channel and receive all updates happened since the snapshot of the document loaded from the database. There is a way to solve this though, we will cover it shortly.

    \n

    Re-sync upon lost continuity

    \n

    Another complexity which is left to the user is the need to react on recovered: false flag provided by the SDK when client can not catch up the state upo re-subscription. This may happen due to channel history retention configuration, or simply because history was lost. In this case our SDKs provide users wasRecovering: true and recovered: false flags, and we suggest re-fetching the document state from the backend in such cases. But while you re-fetch the state you are still receiving real-time updates from the subscription – which leads us to something similar to the problem described above, same race conditions may happen leaving the component in the inconsistent state until reload.

    \n

    Late delivery of real-time updates

    \n

    One more possible problem to discuss is a late delivery of real-time messages.

    \n

    When you want to reliably stream document changes to Centrifugo (without loosing any update, due to temporary network issues for example) and keep the order of changes (to not occasionally apply property addition and deletion is different order on the client side) your best bet is using transactional outbox or CDC approaches. So that changes in the database are made atomic and there is a guarantee that the update will be soon issued to the real-time channel of Centrifuge-based server or Centrifugo. Usually transactional outbox or CDC can also maintain correct order of event processing, thus correct order of publising to the real-time channel.

    \n

    But this means that upon loading a real-time component it may receive non-actual real-time updates from the real-time subscription – due to outbox table or CDC processing lag. We need a way for the client side to understand whether the update must be applied to the document state or not. Sometimes it's possible to understand due to the nature of component. Like receiving an update with some identifier which already exists in the object on client side. But what if update contains deletion of some property of object? This will mean that object may rollback to non-actual state, then will receive next real-time updates which will move it to back to the actual state. We want to avoid such modifications leading to temporary state glitches at all. Not all cases allow having idempotent real-time updates.

    \n

    Even when you are not using outbox/CDC you can still hit a situation of late real-time message delivery. Let's suppose you publish messages to Centrifugal channel synchronously over server publish API reducing the chance of having a lag from the outbox/CDC processing. But the lag may still present. Because while message travelling towards subscriber through Centrifugo, subscriber can load a more freshy initial state from the main database and subscribe to the real-time channel.

    \n

    Core principles of solution

    \n

    In this post we will write a RealTimeDocument JavaScript class designed to synchronize a document state. This class handles initial data loading, real-time updates, and state re-synchronization when required. It should solve the problems described above.

    \n

    The good thing is that this helper class is compact enough to be implemented in any programming language, so you can apply it (or the required part of it) for other languages where we already have real-time SDKs.

    \n

    We will build the helper on top of several core principles:

    \n
      \n
    • The document has an incremental version which is managed atomically and transactionally on the backend.
    • \n
    • Initial state loading returns document state together with the current version, loading happens with at least read committed transactional isolation level (default in many databases, ex. PostgreSQL)
    • \n
    • All real-time updates published to the document channel have the version attached, and updates are published to the channel in the correct version order.
    • \n
    \n

    We already discussed the approach in our Grand tutorial – but now want to generalize it as a re-usable pattern.

    \n

    After writing a RealTimeDocument wrapper we will apply it to a simple example of synchronizing counter increments across multiple devices reliably to demonstrate it works. Eventually we get best from two worlds – leveraging Centrifugo publication cache to avoid excessive load on the backend upon massive reconnect and proper document state in all scenarios.

    \n

    Top-level API of RealTimeDocument

    \n
    const subscription = centrifuge.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription, // Wraps Subscription.
    load: async (): Promise<{ document: any; version: number }> => {
    // Must load the actual document state and version from the database.
    // Ex. return { document: result.document, version: result.version };
    },
    applyUpdate: (currentDocument: any, update: any): any => {
    // Must apply update to the document.
    // currentDocument.value += update.increment;
    // return currentDocument;
    },
    compareVersion: (currentVersion: number, update: any): number | null => {
    // Must compare versions in real-time publication and current doc version.
    // const newVersion = publication.data.version;
    // return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document: any) => {
    // Will be called once the document is loaded for the first time and every time
    // the document state is updated. This is where application may render things
    // based on the document data.
    }
    });

    realTimeDocument.startSync();
    \n

    Implementing solution

    \n

    To address the gap between state load and real-time subscription establishment the obvious solution which is possible with Centrifugal stack is to make the real-time subscription first, and only after that load the state from the backend. This eliminates the possibility to miss messages. But until the state is loaded we need to buffer real-time publications and then apply them to the loaded state.

    \n

    Here is where the concept of having incremental document version helps – we can collect messages in the buffer, and then apply only those with version greater than current document version. So that the object will have the correct state after the initial load.

    \n

    Here is how we can process real-time publications:

    \n
    this.#subscription.on('publication', (ctx) => {
    if (!this.#isLoaded) {
    // Buffer messages until initial state is loaded.
    this.#messageBuffer.push(ctx);
    return;
    }
    // Process new messages immediately if initial state is already loaded.
    const newVersion = this.#compareVersion(ctx.data, this.#version);
    if (newVersion === null) {
    // Skip real-time publication, non actual version.
    return;
    }
    this.#document = this.#applyUpdate(this.#document, ctx.data);
    this.#version = newVersion;
    this.#onChange(this.#document);
    }
    \n

    And we also need to handle subscribed event properly and load the initial document state from the backend:

    \n
    this.#subscription.on('subscribed', (ctx) => {
    if (ctx.wasRecovering) {
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    } else {
    // Load data for the first time.
    this.#loadDocumentApplyBuffered();
    }
    })
    \n

    For the initial load this.#loadDocumentApplyBuffered() will be called. Here is how it may look like:

    \n
    async #loadDocumentApplyBuffered() {
    try {
    const result = await this.#load();
    this.#document = result.document;
    this.#version = result.version;
    this.#isLoaded = true;
    this.#processBufferedMessages();
    } catch (error) {
    // Retry the loading, in the final snippet it's implemented
    // and uses exponential backoff for the retry process.
    }
    }
    \n

    After loading the state we prosess buffered real-time publications inside #processBufferedMessages method:

    \n
    #processBufferedMessages() {
    this.#messageBuffer.forEach((msg) => {
    const newVersion = this.#compareVersion(msg, this.#version);
    if (newVersion) { // Otherwise, skip buffered publication.
    this.#document = this.#applyUpdate(this.#document, msg.data);
    this.#version = newVersion;
    }
    });
    // Clear the buffer after processing.
    this.#messageBuffer = [];
    // Only call onChange with final document state.
    this.#onChange(this.#document);
    }
    \n

    This way the initial state is loaded correctly. Note also, that version comparisons also help with handling late delivered real-time updates – we now simply skip them inside on('publication') callback.

    \n

    Let's go back and look how to manage stream continuity loss:

    \n
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    \n

    In this case we call #reSync method:

    \n
    #reSync() {
    this.#isLoaded = false; // Reset the flag to collect new messages to the buffer.
    this.#messageBuffer = [];
    this.#loadDocumentApplyBuffered();
    }
    \n

    It basically clears up the class state and calls #loadDocumentApplyBuffered again – repeating the initial sync procedure.

    \n

    That's it. Here is a full code for the RealTimeDocument class. Note, it also contains backoff implementation to handle possible error while loading the document state from the backend endpoint.

    \n

    Let's apply it

    \n

    I've made a POC with Centrifuge library to make sure this works.

    \n

    In that example I tried to apply RealTimeDocument class to synchronize state of the counter. Periodically timer is incremented on a random value in range [0,9] on the backend and these increments are published to the real-time channel. Note, I could simply publish counter value in every publication over WebSocket – but intentionally decided to send counter increments instead. To make sure nothing is lost during state synchronization so counter value is always correct on the client side.

    \n

    Here is a demo:

    \n
    \n

    Let's look at how RealTimeDocument class was used in the example:

    \n
    const counterContainer = document.getElementById(\"counter\");

    const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});
    const subscription = client.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription,
    load: async () => {
    const response = await fetch('/api/counter');
    const result = await response.json();
    return { document: result.value, version: result.version };
    },
    applyUpdate: (document, update) => {
    document += update.increment
    return document
    },
    compareVersion: (currentVersion, update) => {
    const newVersion = update.version;
    return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document) => {
    counterContainer.textContent = document;
    },
    debug: true,
    });
    client.connect();

    // Note – we can call sync even before connect.
    realTimeDocument.startSync();
    \n

    Things to observe:

    \n
      \n
    • We return {\"version\":4823,\"value\":21656} from /api/counter
    • \n
    • Send {\"version\":4824,\"increment\":9} over real-time channel
    • \n
    • Counter updated every 250 milliseconds, history size is 20, retention 10 seconds
    • \n
    • Upon going offline for a short period we see that /api/counter endpoint not called at all - state fully cought up from Centrifugo history stream
    • \n
    • Upon going offline for a longer period Centrifugo was not able to recover the state, so we re-fetched data from scratch and attached to the stream again.
    • \n
    \n

    Conclusion

    \n

    In this post, we walked through a practical implementation of a RealTimeDocument class using Centrifugal stack for the real-time state synchronization to achieve proper eventually consistent state of the document when using real-time updates. We mentioned possible gotchas when trying to sync the state in real-time and described a generic solution to it.

    \n

    You don't need to always follow the solution here. As I mentioned it's possible that your app does not require handling all these edge cases, or they could be handled in alternative ways – this heavily depends on your app business logic.

    \n

    Note, that with some changes you can make RealTimeDocument class to behave properly and support graceful degradation behaviour. If there are issues with real-time subscription you can still load the document and display it, and then re-sync the state as soon as a real-time system becomes available (successful subscription).

    ", + "url": "https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync", + "title": "Proper real-time document state synchronization within Centrifugal ecosystem", + "summary": "A simple yet cool example of document state synchronization on top of Centrifugal stack. The end goal is to effectively and reliably synchronize document state to application users – to avoid race condition between state load and updates coming from the real-time subscription.", + "date_modified": "2024-06-03T00:00:00.000Z", + "author": { + "name": "Alexander Emelin" + }, + "tags": [ + "centrifugo", + "centrifuge", + "websocket", + "docsync" + ] + }, { "id": "https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments", "content_html": "\n

    Optimizing data transfer over WebSocket connections can significantly reduce bandwidth costs. Compressing data usually leads to memory and CPU resource usage overhead – but in many cases it worth doing anyway since it positively impacts the final bill from the provider (bandwidth cost reduction overweights resource usage increase).

    \n

    Centrifugo v5.4.0 introduced delta compression feature. But before implementing it we wanted a playground which could demonstrate the potential benefit of using delta compression in Centrifugo channels.

    \n

    This post outlines our approach to estimating the potential profit from implementing delta compression. It demonstrates the reduction in data transfer using once concrete use case across various configurations, including different Centrifugo protocol formats and the additional use of WebSocket permessage-deflate compression. Although these numbers can vary significantly depending on the data, we believe the results are valuable for providing a general understanding of Centrifugo compression options. This information can help Centrifugo users apply these insights to their use cases.

    \n

    About delta compression

    \n

    \"delta

    \n

    For a good overview of delta compression topic for the real-time messaging applications I suggest starting with a blog post in Ably engineeiring blog.

    \n

    Centrifugo is very similar to Ably in many aspects (though self-hosted), so everything said in the linked post equally applies to Centrifugo use cases too. Though we have differences in the final implementation, one notable is that we are using Fossil delta algorithm in Centrifugo instead of VCDIFF. The reason over VCDIFF was mainly two factors:

    \n
      \n
    • availability of several Fossil delta implementations, specifically there are good libraries for Go (see shadowspore/fossil-delta), and for Javascript - fossil-delta-js.
    • \n
    • the compactness of the algorithm implementation – under 500 lines of code in JavaScript
    • \n
    \n

    The compactness property is nice because there are no OSS Fossil implementations for Java, Dart and Swift – languages we have SDKs for – so we may have to implement this algorithm in the future ourselves.

    \n

    Having said this all, let's proceed to the description of experiment we did to understand possible benefits of various compression techniques, and delta compression in particular.

    \n

    Experiment Overview

    \n

    In the experiment, we simulated a football match, sending the entire game state over a WebSocket connection upon every match event. Our compression playground looks like this:

    \n\n

    It visualizes only the score, but under the hood there are other game changes happen – will be shown below.

    \n

    We tested various configurations to evaluate the effectiveness of data compression if different cases. In each setup the same game data was sent over the wire. The data then was captured using WireShark with the filter:

    \n
    tcp.srcport == 8000 && websocket
    \n

    This is how WebSocket packets look in Wireshark when applying a filter mentioned above:

    \n

    \"wireshark\"

    \n

    Bytes captured show the entire overhead from packets in the game channel going from server to client (including TCP/IP overhead).

    \n

    The source code of the experiment may be found on Github as a Centrifuge library example. You can run it to inspect the exact WebSocket frames in each scenario.

    \n

    To give reader a general idea about data, we sent 30 publications with the entire football game state, for example here is a first message in a match (2 teams, 11 players):

    \n
    Click to see the data

    {
    \"homeTeam\":{
    \"name\":\"Real Madrid\",
    \"players\":[
    {
    \"name\":\"John Doe\"
    },
    {
    \"name\":\"Jane Smith\"
    },
    {
    \"name\":\"Alex Johnson\"
    },
    {
    \"name\":\"Chris Lee\"
    },
    {
    \"name\":\"Pat Kim\"
    },
    {
    \"name\":\"Sam Morgan\"
    },
    {
    \"name\":\"Jamie Brown\"
    },
    {
    \"name\":\"Casey Davis\"
    },
    {
    \"name\":\"Morgan Garcia\"
    },
    {
    \"name\":\"Taylor White\"
    },
    {
    \"name\":\"Jordan Martinez\"
    }
    ]
    },
    \"awayTeam\":{
    \"name\":\"Barcelona\",
    \"players\":[
    {
    \"name\":\"Robin Wilson\"
    },
    {
    \"name\":\"Drew Taylor\",
    \"events\":[
    {
    \"type\":\"RED_CARD\"
    }
    ]
    },
    {
    \"name\":\"Jessie Bailey\"
    },
    {
    \"name\":\"Casey Flores\"
    },
    {
    \"name\":\"Jordan Walker\"
    },
    {
    \"name\":\"Charlie Green\"
    },
    {
    \"name\":\"Alex Adams\"
    },
    {
    \"name\":\"Morgan Thompson\"
    },
    {
    \"name\":\"Taylor Clark\"
    },
    {
    \"name\":\"Jordan Hernandez\"
    },
    {
    \"name\":\"Jamie Lewis\"
    }
    ]
    }
    }

    \n

    Then we send intermediary states – someone scores goal, gets yellow card, being subsctituted. And here is the end message in simulation (final scores, final events attached to corresponding players):

    \n
    Click to see the data

    {
    \"homeTeam\":{
    \"name\":\"Real Madrid\",
    \"score\":3,
    \"players\":[
    {
    \"name\":\"John Doe\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":6
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":39
    }
    ]
    },
    {
    \"name\":\"Jane Smith\"
    },
    {
    \"name\":\"Alex Johnson\"
    },
    {
    \"name\":\"Chris Lee\",
    \"events\":[
    {
    \"type\":\"GOAL\",
    \"minute\":84
    }
    ]
    },
    {
    \"name\":\"Pat Kim\"
    },
    {
    \"name\":\"Sam Morgan\"
    },
    {
    \"name\":\"Jamie Brown\",
    \"events\":[
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":9
    }
    ]
    },
    {
    \"name\":\"Casey Davis\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":81
    }
    ]
    },
    {
    \"name\":\"Morgan Garcia\",
    \"events\":[
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":15
    },
    {
    \"type\":\"GOAL\",
    \"minute\":30
    },
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":57
    },
    {
    \"type\":\"GOAL\",
    \"minute\":62
    },
    {
    \"type\":\"RED_CARD\",
    \"minute\":66
    }
    ]
    },
    {
    \"name\":\"Taylor White\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":18
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":42
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":45
    },
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":69
    },
    {
    \"type\":\"RED_CARD\",
    \"minute\":72
    }
    ]
    },
    {
    \"name\":\"Jordan Martinez\",
    \"events\":[
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":21
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":24
    }
    ]
    }
    ]
    },
    \"awayTeam\":{
    \"name\":\"Barcelona\",
    \"score\":3,
    \"players\":[
    {
    \"name\":\"Robin Wilson\"
    },
    {
    \"name\":\"Drew Taylor\",
    \"events\":[
    {
    \"type\":\"RED_CARD\"
    },
    {
    \"type\":\"GOAL\",
    \"minute\":12
    }
    ]
    },
    {
    \"name\":\"Jessie Bailey\"
    },
    {
    \"name\":\"Casey Flores\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":78
    }
    ]
    },
    {
    \"name\":\"Jordan Walker\",
    \"events\":[
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":33
    }
    ]
    },
    {
    \"name\":\"Charlie Green\",
    \"events\":[
    {
    \"type\":\"GOAL\",
    \"minute\":51
    },
    {
    \"type\":\"GOAL\",
    \"minute\":60
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":75
    }
    ]
    },
    {
    \"name\":\"Alex Adams\"
    },
    {
    \"name\":\"Morgan Thompson\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":27
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":48
    }
    ]
    },
    {
    \"name\":\"Taylor Clark\",
    \"events\":[
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":3
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":87
    }
    ]
    },
    {
    \"name\":\"Jordan Hernandez\"
    },
    {
    \"name\":\"Jamie Lewis\",
    \"events\":[
    {
    \"type\":\"YELLOW_CARD\",
    \"minute\":36
    },
    {
    \"type\":\"SUBSTITUTE\",
    \"minute\":54
    }
    ]
    }
    ]
    }
    }

    \n

    When we used Protobuf encoding for game state we serialized the data according to this Protobuf schema:

    \n
    Click to see the Protobuf schema for the game state

    syntax = \"proto3\";

    package centrifugal.centrifuge.examples.compression_playground;

    option go_package = \"./;apppb\";

    enum EventType {
    UNKNOWN = 0; // Default value, should not be used
    GOAL = 1;
    YELLOW_CARD = 2;
    RED_CARD = 3;
    SUBSTITUTE = 4;
    }

    message Event {
    EventType type = 1;
    int32 minute = 2;
    }

    message Player {
    string name = 1;
    repeated Event events = 2;
    }

    message Team {
    string name = 1;
    int32 score = 2;
    repeated Player players = 3;
    }

    message Match {
    int32 id = 1;
    Team home_team = 2;
    Team away_team = 3;
    }

    \n

    Results Breakdown

    \n

    Below are the results of our experiment, comparing different protocols and compression settings:

    \n
    ProtocolCompressionDeltaBytes sentPercentage
    JSON over JSONNoNo40251100.0 (base)
    JSON over JSONYesNo1566938.93
    JSON over JSONNoYes604315.01
    JSON over JSONYesYes536013.32
    ----------
    JSON over ProtobufNoNo3918097.34
    JSON over ProtobufYesNo1554238.61
    JSON over ProtobufNoYes428710.65
    JSON over ProtobufYesYes412610.25
    ----------
    Protobuf over ProtobufNoNo1656241.15
    Protobuf over ProtobufYesNo1311532.58
    Protobuf over ProtobufNoYes438210.89
    Protobuf over ProtobufYesYes447311.11
    \n

    Results analysis

    \n

    Let's now discuss the results we observed in detail.

    \n

    JSON over JSON

    \n

    In this case we are sending JSON data with football match game state over JSON Centrifugal protocol.

    \n
      \n
    1. \n

      JSON over JSON (No Compression, No Delta)\nBytes Sent: 40251\nPercentage: 100.0%\nAnalysis: This is a baseline scenario, with no compression and no delta, results in the highest amount of data being sent. But very straightforward to implement.

      \n
    2. \n
    3. \n

      JSON over JSON (With Compression, No Delta)\nBytes Sent: 15669\nPercentage: 38.93%\nAnalysis: Enabling compression reduces the data size significantly to 38.93% of the original, showcasing the effectiveness of deflate compression. See how to configure compression in Centrifugo, note that it comes with CPU and memory overhead which depends on your load profile.

      \n
    4. \n
    5. \n

      JSON over JSON (No Compression, With Delta)\nBytes Sent: 6043\nPercentage: 15.01%\nAnalysis: Using delta compression without deflate compression reduces data size to 15.01% for this use case, only changes are being sent after the initial full payload. See how to enable delta compression in channels in Centrifugo. The nice thing about using delta compression instead of deflate compression is that deltas require less and more predictable resource overhead.

      \n
    6. \n
    7. \n

      JSON over JSON (With Compression and Delta)\nBytes Sent: 5360\nPercentage: 13.32%\nAnalysis: Combining both compression and delta further reduces the data size to 13.32%, achieving the highest efficiency in this category. The benefit is not huge, because we already send small deltas here.

      \n
    8. \n
    \n

    JSON over Protobuf

    \n

    In this case we are sending JSON data with football match game state over Protobuf Centrifugal protocol.

    \n
      \n
    1. \n

      JSON over Protobuf (No Compression, No Delta)\nBytes Sent: 39180\nPercentage: 97.34%\nAnalysis: Switching to Protobuf encoding of Centrifugo protocol but still sending JSON data slightly reduces the data size to 97.34% of the JSON over JSON baseline. The benefit here comes from the fact Centrifugo does not introduce a lot of its own protocol overhead – Protobuf is more compact. But we still send JSON data as Protobuf payloads – that's why it's generally comparable with a baseline.

      \n
    2. \n
    3. \n

      JSON over Protobuf (With Compression, No Delta)\nBytes Sent: 15542\nPercentage: 38.61%\nAnalysis: Compression with Protobuf encoding brings similar benefits as with JSON, reducing the data size to 38.61%.

      \n
    4. \n
    5. \n

      JSON over Protobuf (No Compression, With Delta)\nBytes Sent: 4287\nPercentage: 10.65%\nAnalysis: Delta compression with Protobuf is effective, reducing data to 10.65%. It's almost x10 reduction in bandwidth compared to the baseline!

      \n
    6. \n
    \n

    I guess at this point you may be curious how delta frames look like in case of JSON protocol. Here is a screenshot:

    \n

    \"delta

    \n
      \n
    1. JSON over Protobuf (With Compression and Delta)\nBytes Sent: 4126\nPercentage: 10.25%\nAnalysis: This combination provides the best results for JSON over Protobuf, reducing data size to 10.25% from the baseline.
    2. \n
    \n

    Protobuf over Protobuf

    \n

    In this case we are sending Protobuf binary data with football match game state over Protobuf Centrifugal protocol.

    \n
      \n
    1. \n

      Protobuf over Protobuf (No Compression, No Delta)\nBytes Sent: 16562\nPercentage: 41.15%\nAnalysis: Using Protobuf for both encoding and transmission without any compression or delta reduces data size to 41.15%. So you may get the most efficient setup with nice bandwidth reduction. But the cost is more complex data encoding.

      \n
    2. \n
    3. \n

      Protobuf over Protobuf (With Compression, No Delta)\nBytes Sent: 13115\nPercentage: 32.58%\nAnalysis: Compression reduces the data size to 32.58%. Note, that in this case it's not very different from JSON case.

      \n
    4. \n
    5. \n

      Protobuf over Protobuf (No Compression, With Delta)\nBytes Sent: 4382\nPercentage: 10.89%\nAnalysis: Delta compression is again very effective here, reducing the data size to 10.89%. Again - comparable to JSON case.

      \n
    6. \n
    7. \n

      Protobuf over Protobuf (With Compression and Delta)\nBytes Sent: 4473\nPercentage: 11.11%\nAnalysis: Combining both methods results in a data size of 11.11%. Even more than in JSON case. That's bacause binary data is not compressed very well with deflate algorithm.

      \n
    8. \n
    \n

    Conclusion

    \n
      \n
    • \n

      WebSocket permessage-deflate compression significantly reduces the amount of data transferred over WebSocket connections. While it incurs CPU and memory overhead, it may be still worth using from a total cost perspective.

      \n
    • \n
    • \n

      Delta compression makes perfect sense for channels where data changes only slightly between publications. In our experiment, it resulted in a tenfold reduction in bandwidth usage.

      \n
    • \n
    • \n

      Using binary data in combination with the Centrifugo Protobuf protocol provides substantial bandwidth reduction even without deflate or delta compression. However, this comes at the cost of increased data format complexity. An additional benefit of using the Centrifugo Protobuf protocol is its faster marshalling and unmarshalling on the server side compared to the JSON protocol.

      \n
    • \n
    \n

    For Centrifugo, these results highlighted the potential of implementing delta compression, so we proceeded with it. The benefit depends on the nature of the data being sent – you can achieve even greater savings if you have larger messages that are very similar to each other.

    ", diff --git a/blog/rss.xml b/blog/rss.xml index 9f9506aaa..b3d6b0b51 100644 --- a/blog/rss.xml +++ b/blog/rss.xml @@ -4,11 +4,88 @@ https://centrifugal.dev/blog - Thu, 30 May 2024 00:00:00 GMT + Mon, 03 Jun 2024 00:00:00 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en Centrifugal Labs LTD + + <![CDATA[Proper real-time document state synchronization within Centrifugal ecosystem]]> + https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync + https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync + Mon, 03 Jun 2024 00:00:00 GMT + + +

    Centrifugo and its main building block Centrifuge library for Go both provide a way for clients to receive a stream of events in channels using Subscription objects. Also, there is an automatic history recovery feature which allows clients catching up with missed publications after the reconnect to the WebSocket server and restore the state of a real-time component. While the continuity in the stream is not broken clients can avoid re-fetching a state from the main application database – which optimizes a scenario when many real-time connections reconnect all within a short time interval (for example, during a load balancer restart) by reducing the excessive load on the application database.

    +

    Usually, our users who use recovery features load the document state from the backend, then establish a real-time Subscription to apply changes to the component state coming from the real-time channel. After that the component stays synchronized, even after network issues – due to Centrifugo recovery feature the document state becomes actual again since client catches up the state from the channel history stream.

    +

    There are several hidden complexities in the process though and things left for users to implement. We want to address those here.

    +

    Complexities in state sync

    +

    Gap in time

    +

    The first edge case comes from the fact that there is a possible gap in time between initial loading of the state from the main app database and real-time subscription establishment. Some messages could be published in between of state loading and real-time subscription establishment. So there is a chance that due to this gap in time the component will live in the inconsistent state until the next application page reload. For many apps this is not critical at all, or due to message rates happens very rarely. But in this post we will look at the possible approach to avoid such a case.

    +

    Or imagine a situation when state is loaded, but real-time subscription is delayed due to some temporary error. This increases a gap in time and a chance to miss an intermediary update.

    +

    Centrifugo channel stream offsets are not binded to the application business models in any way, so it's not possible to initially subscribe to the real-time channel and receive all updates happened since the snapshot of the document loaded from the database. There is a way to solve this though, we will cover it shortly.

    +

    Re-sync upon lost continuity

    +

    Another complexity which is left to the user is the need to react on recovered: false flag provided by the SDK when client can not catch up the state upo re-subscription. This may happen due to channel history retention configuration, or simply because history was lost. In this case our SDKs provide users wasRecovering: true and recovered: false flags, and we suggest re-fetching the document state from the backend in such cases. But while you re-fetch the state you are still receiving real-time updates from the subscription – which leads us to something similar to the problem described above, same race conditions may happen leaving the component in the inconsistent state until reload.

    +

    Late delivery of real-time updates

    +

    One more possible problem to discuss is a late delivery of real-time messages.

    +

    When you want to reliably stream document changes to Centrifugo (without loosing any update, due to temporary network issues for example) and keep the order of changes (to not occasionally apply property addition and deletion is different order on the client side) your best bet is using transactional outbox or CDC approaches. So that changes in the database are made atomic and there is a guarantee that the update will be soon issued to the real-time channel of Centrifuge-based server or Centrifugo. Usually transactional outbox or CDC can also maintain correct order of event processing, thus correct order of publising to the real-time channel.

    +

    But this means that upon loading a real-time component it may receive non-actual real-time updates from the real-time subscription – due to outbox table or CDC processing lag. We need a way for the client side to understand whether the update must be applied to the document state or not. Sometimes it's possible to understand due to the nature of component. Like receiving an update with some identifier which already exists in the object on client side. But what if update contains deletion of some property of object? This will mean that object may rollback to non-actual state, then will receive next real-time updates which will move it to back to the actual state. We want to avoid such modifications leading to temporary state glitches at all. Not all cases allow having idempotent real-time updates.

    +

    Even when you are not using outbox/CDC you can still hit a situation of late real-time message delivery. Let's suppose you publish messages to Centrifugal channel synchronously over server publish API reducing the chance of having a lag from the outbox/CDC processing. But the lag may still present. Because while message travelling towards subscriber through Centrifugo, subscriber can load a more freshy initial state from the main database and subscribe to the real-time channel.

    +

    Core principles of solution

    +

    In this post we will write a RealTimeDocument JavaScript class designed to synchronize a document state. This class handles initial data loading, real-time updates, and state re-synchronization when required. It should solve the problems described above.

    +

    The good thing is that this helper class is compact enough to be implemented in any programming language, so you can apply it (or the required part of it) for other languages where we already have real-time SDKs.

    +

    We will build the helper on top of several core principles:

    +
      +
    • The document has an incremental version which is managed atomically and transactionally on the backend.
    • +
    • Initial state loading returns document state together with the current version, loading happens with at least read committed transactional isolation level (default in many databases, ex. PostgreSQL)
    • +
    • All real-time updates published to the document channel have the version attached, and updates are published to the channel in the correct version order.
    • +
    +

    We already discussed the approach in our Grand tutorial – but now want to generalize it as a re-usable pattern.

    +

    After writing a RealTimeDocument wrapper we will apply it to a simple example of synchronizing counter increments across multiple devices reliably to demonstrate it works. Eventually we get best from two worlds – leveraging Centrifugo publication cache to avoid excessive load on the backend upon massive reconnect and proper document state in all scenarios.

    +

    Top-level API of RealTimeDocument

    +
    const subscription = centrifuge.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription, // Wraps Subscription.
    load: async (): Promise<{ document: any; version: number }> => {
    // Must load the actual document state and version from the database.
    // Ex. return { document: result.document, version: result.version };
    },
    applyUpdate: (currentDocument: any, update: any): any => {
    // Must apply update to the document.
    // currentDocument.value += update.increment;
    // return currentDocument;
    },
    compareVersion: (currentVersion: number, update: any): number | null => {
    // Must compare versions in real-time publication and current doc version.
    // const newVersion = publication.data.version;
    // return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document: any) => {
    // Will be called once the document is loaded for the first time and every time
    // the document state is updated. This is where application may render things
    // based on the document data.
    }
    });

    realTimeDocument.startSync();
    +

    Implementing solution

    +

    To address the gap between state load and real-time subscription establishment the obvious solution which is possible with Centrifugal stack is to make the real-time subscription first, and only after that load the state from the backend. This eliminates the possibility to miss messages. But until the state is loaded we need to buffer real-time publications and then apply them to the loaded state.

    +

    Here is where the concept of having incremental document version helps – we can collect messages in the buffer, and then apply only those with version greater than current document version. So that the object will have the correct state after the initial load.

    +

    Here is how we can process real-time publications:

    +
    this.#subscription.on('publication', (ctx) => {
    if (!this.#isLoaded) {
    // Buffer messages until initial state is loaded.
    this.#messageBuffer.push(ctx);
    return;
    }
    // Process new messages immediately if initial state is already loaded.
    const newVersion = this.#compareVersion(ctx.data, this.#version);
    if (newVersion === null) {
    // Skip real-time publication, non actual version.
    return;
    }
    this.#document = this.#applyUpdate(this.#document, ctx.data);
    this.#version = newVersion;
    this.#onChange(this.#document);
    }
    +

    And we also need to handle subscribed event properly and load the initial document state from the backend:

    +
    this.#subscription.on('subscribed', (ctx) => {
    if (ctx.wasRecovering) {
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    } else {
    // Load data for the first time.
    this.#loadDocumentApplyBuffered();
    }
    })
    +

    For the initial load this.#loadDocumentApplyBuffered() will be called. Here is how it may look like:

    +
    async #loadDocumentApplyBuffered() {
    try {
    const result = await this.#load();
    this.#document = result.document;
    this.#version = result.version;
    this.#isLoaded = true;
    this.#processBufferedMessages();
    } catch (error) {
    // Retry the loading, in the final snippet it's implemented
    // and uses exponential backoff for the retry process.
    }
    }
    +

    After loading the state we prosess buffered real-time publications inside #processBufferedMessages method:

    +
    #processBufferedMessages() {
    this.#messageBuffer.forEach((msg) => {
    const newVersion = this.#compareVersion(msg, this.#version);
    if (newVersion) { // Otherwise, skip buffered publication.
    this.#document = this.#applyUpdate(this.#document, msg.data);
    this.#version = newVersion;
    }
    });
    // Clear the buffer after processing.
    this.#messageBuffer = [];
    // Only call onChange with final document state.
    this.#onChange(this.#document);
    }
    +

    This way the initial state is loaded correctly. Note also, that version comparisons also help with handling late delivered real-time updates – we now simply skip them inside on('publication') callback.

    +

    Let's go back and look how to manage stream continuity loss:

    +
    if (ctx.recovered) {
    // Successfully re-attached to a stream, nothing else to do.
    } else {
    // Re-syncing due to failed recovery.
    this.#reSync();
    }
    +

    In this case we call #reSync method:

    +
    #reSync() {
    this.#isLoaded = false; // Reset the flag to collect new messages to the buffer.
    this.#messageBuffer = [];
    this.#loadDocumentApplyBuffered();
    }
    +

    It basically clears up the class state and calls #loadDocumentApplyBuffered again – repeating the initial sync procedure.

    +

    That's it. Here is a full code for the RealTimeDocument class. Note, it also contains backoff implementation to handle possible error while loading the document state from the backend endpoint.

    +

    Let's apply it

    +

    I've made a POC with Centrifuge library to make sure this works.

    +

    In that example I tried to apply RealTimeDocument class to synchronize state of the counter. Periodically timer is incremented on a random value in range [0,9] on the backend and these increments are published to the real-time channel. Note, I could simply publish counter value in every publication over WebSocket – but intentionally decided to send counter increments instead. To make sure nothing is lost during state synchronization so counter value is always correct on the client side.

    +

    Here is a demo:

    +
    +

    Let's look at how RealTimeDocument class was used in the example:

    +
    const counterContainer = document.getElementById("counter");

    const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});
    const subscription = client.newSubscription('counter', {});

    const realTimeDocument = new RealTimeDocument({
    subscription,
    load: async () => {
    const response = await fetch('/api/counter');
    const result = await response.json();
    return { document: result.value, version: result.version };
    },
    applyUpdate: (document, update) => {
    document += update.increment
    return document
    },
    compareVersion: (currentVersion, update) => {
    const newVersion = update.version;
    return newVersion > currentVersion ? newVersion : null;
    },
    onChange: (document) => {
    counterContainer.textContent = document;
    },
    debug: true,
    });
    client.connect();

    // Note – we can call sync even before connect.
    realTimeDocument.startSync();
    +

    Things to observe:

    +
      +
    • We return {"version":4823,"value":21656} from /api/counter
    • +
    • Send {"version":4824,"increment":9} over real-time channel
    • +
    • Counter updated every 250 milliseconds, history size is 20, retention 10 seconds
    • +
    • Upon going offline for a short period we see that /api/counter endpoint not called at all - state fully cought up from Centrifugo history stream
    • +
    • Upon going offline for a longer period Centrifugo was not able to recover the state, so we re-fetched data from scratch and attached to the stream again.
    • +
    +

    Conclusion

    +

    In this post, we walked through a practical implementation of a RealTimeDocument class using Centrifugal stack for the real-time state synchronization to achieve proper eventually consistent state of the document when using real-time updates. We mentioned possible gotchas when trying to sync the state in real-time and described a generic solution to it.

    +

    You don't need to always follow the solution here. As I mentioned it's possible that your app does not require handling all these edge cases, or they could be handled in alternative ways – this heavily depends on your app business logic.

    +

    Note, that with some changes you can make RealTimeDocument class to behave properly and support graceful degradation behaviour. If there are issues with real-time subscription you can still load the document and display it, and then re-sync the state as soon as a real-time system becomes available (successful subscription).

    ]]>
    + centrifugo + centrifuge + websocket + docsync +
    <![CDATA[Experimenting with real-time data compression by simulating a football match events]]> https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments diff --git a/blog/tags.html b/blog/tags.html index d61ba6339..d5e765f00 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -15,10 +15,10 @@ - - + + - + \ No newline at end of file diff --git a/blog/tags/authentication.html b/blog/tags/authentication.html index 450dc8681..5e753efb7 100644 --- a/blog/tags/authentication.html +++ b/blog/tags/authentication.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "authentication"

    View All Tags
    diff --git a/blog/tags/benthos.html b/blog/tags/benthos.html index 8605a8313..5bdd86ec8 100644 --- a/blog/tags/benthos.html +++ b/blog/tags/benthos.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "benthos"

    View All Tags
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    diff --git a/blog/tags/centrifuge.html b/blog/tags/centrifuge.html index 9c0db6f2d..9e639287c 100644 --- a/blog/tags/centrifuge.html +++ b/blog/tags/centrifuge.html @@ -3,7 +3,7 @@ -2 posts tagged with "centrifuge" | Centrifugo +3 posts tagged with "centrifuge" | Centrifugo @@ -15,10 +15,10 @@ - - + + -

    2 posts tagged with "centrifuge"

    View All Tags
    by Centrifugal team
    Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second
    +

    3 posts tagged with "centrifuge"

    View All Tags
    by Centrifugal team
    Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second
    \ No newline at end of file diff --git a/blog/tags/centrifugo.html b/blog/tags/centrifugo.html index 7f630c8bd..45e79d59e 100644 --- a/blog/tags/centrifugo.html +++ b/blog/tags/centrifugo.html @@ -3,7 +3,7 @@ -14 posts tagged with "centrifugo" | Centrifugo +15 posts tagged with "centrifugo" | Centrifugo @@ -15,10 +15,10 @@ - - + + -

    14 posts tagged with "centrifugo"

    View All Tags
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    by Centrifugal team
    We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.
    by Centrifugal team
    Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    +

    15 posts tagged with "centrifugo"

    View All Tags
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    by Centrifugal team
    We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.
    by Centrifugal team
    Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    \ No newline at end of file diff --git a/blog/tags/compression.html b/blog/tags/compression.html index f5b2cfba7..b674e67e2 100644 --- a/blog/tags/compression.html +++ b/blog/tags/compression.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "compression"

    View All Tags
    diff --git a/blog/tags/django.html b/blog/tags/django.html index 14faeb046..1cabecf4c 100644 --- a/blog/tags/django.html +++ b/blog/tags/django.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "django"

    View All Tags
    diff --git a/blog/tags/docsync.html b/blog/tags/docsync.html new file mode 100644 index 000000000..55c5644ce --- /dev/null +++ b/blog/tags/docsync.html @@ -0,0 +1,24 @@ + + + + + +One post tagged with "docsync" | Centrifugo + + + + + + + + + + + + + + + +

    One post tagged with "docsync"

    View All Tags
    + + \ No newline at end of file diff --git a/blog/tags/go.html b/blog/tags/go.html index 2cdc97276..ccbbe4945 100644 --- a/blog/tags/go.html +++ b/blog/tags/go.html @@ -15,8 +15,8 @@ - - + +

    5 posts tagged with "go"

    View All Tags
    by Centrifugal team
    Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second
    diff --git a/blog/tags/grpc.html b/blog/tags/grpc.html index 7540f4ab9..0c47a1e34 100644 --- a/blog/tags/grpc.html +++ b/blog/tags/grpc.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "grpc"

    View All Tags
    diff --git a/blog/tags/interview.html b/blog/tags/interview.html index f93dfca58..a90dd63ae 100644 --- a/blog/tags/interview.html +++ b/blog/tags/interview.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "interview"

    View All Tags
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    diff --git a/blog/tags/keycloak.html b/blog/tags/keycloak.html index 66fa937e4..f574d7045 100644 --- a/blog/tags/keycloak.html +++ b/blog/tags/keycloak.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "keycloak"

    View All Tags
    diff --git a/blog/tags/laravel.html b/blog/tags/laravel.html index 7432c9235..c7815ef52 100644 --- a/blog/tags/laravel.html +++ b/blog/tags/laravel.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "laravel"

    View All Tags
    diff --git a/blog/tags/loki.html b/blog/tags/loki.html index ab5e0b30e..183dd0acb 100644 --- a/blog/tags/loki.html +++ b/blog/tags/loki.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "loki"

    View All Tags
    diff --git a/blog/tags/php.html b/blog/tags/php.html index 82503ff66..6a309759d 100644 --- a/blog/tags/php.html +++ b/blog/tags/php.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "php"

    View All Tags
    diff --git a/blog/tags/pro.html b/blog/tags/pro.html index b5bf86bde..84b53cacf 100644 --- a/blog/tags/pro.html +++ b/blog/tags/pro.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "pro"

    View All Tags
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    diff --git a/blog/tags/proxy.html b/blog/tags/proxy.html index 4ec21aa56..42dad1ba1 100644 --- a/blog/tags/proxy.html +++ b/blog/tags/proxy.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "proxy"

    View All Tags
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    diff --git a/blog/tags/push-notifications.html b/blog/tags/push-notifications.html index 066b32fe5..0a6bf6926 100644 --- a/blog/tags/push-notifications.html +++ b/blog/tags/push-notifications.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "push notifications"

    View All Tags
    by Alexander Emelin
    We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.
    diff --git a/blog/tags/quic.html b/blog/tags/quic.html index 05637db88..82df6e6d4 100644 --- a/blog/tags/quic.html +++ b/blog/tags/quic.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "quic"

    View All Tags
    diff --git a/blog/tags/redis.html b/blog/tags/redis.html index e64d5b0dc..69a966c35 100644 --- a/blog/tags/redis.html +++ b/blog/tags/redis.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "redis"

    View All Tags
    diff --git a/blog/tags/release.html b/blog/tags/release.html index d9b129005..404210977 100644 --- a/blog/tags/release.html +++ b/blog/tags/release.html @@ -15,8 +15,8 @@ - - + +

    3 posts tagged with "release"

    View All Tags
    by Centrifugal team
    We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.
    by Centrifugal team
    Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.
    diff --git a/blog/tags/sso.html b/blog/tags/sso.html index 71aba497a..bba3caefb 100644 --- a/blog/tags/sso.html +++ b/blog/tags/sso.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "sso"

    View All Tags
    diff --git a/blog/tags/tutorial.html b/blog/tags/tutorial.html index 4ed9fc724..1e1b34597 100644 --- a/blog/tags/tutorial.html +++ b/blog/tags/tutorial.html @@ -15,8 +15,8 @@ - - + +

    5 posts tagged with "tutorial"

    View All Tags
    by Alexander Emelin
    In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.
    by Alexander Emelin
    In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.
    diff --git a/blog/tags/usecase.html b/blog/tags/usecase.html index 147eee106..564f939ac 100644 --- a/blog/tags/usecase.html +++ b/blog/tags/usecase.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "usecase"

    View All Tags
    by Centrifugal + RabbitX
    In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.
    diff --git a/blog/tags/websocket.html b/blog/tags/websocket.html index 9c11d9afb..e3354c135 100644 --- a/blog/tags/websocket.html +++ b/blog/tags/websocket.html @@ -3,7 +3,7 @@ -2 posts tagged with "websocket" | Centrifugo +3 posts tagged with "websocket" | Centrifugo @@ -15,10 +15,10 @@ - - + + -

    2 posts tagged with "websocket"

    View All Tags
    +

    3 posts tagged with "websocket"

    View All Tags
    \ No newline at end of file diff --git a/blog/tags/webtransport.html b/blog/tags/webtransport.html index 36472d8b5..7a697919e 100644 --- a/blog/tags/webtransport.html +++ b/blog/tags/webtransport.html @@ -15,8 +15,8 @@ - - + +

    One post tagged with "webtransport"

    View All Tags
    diff --git a/components/Highlight.html b/components/Highlight.html index bf8a0d819..0349431c0 100644 --- a/components/Highlight.html +++ b/components/Highlight.html @@ -15,8 +15,8 @@ - - + +

    diff --git a/components/logo.html b/components/logo.html index bef4ef2ce..860b5c996 100644 --- a/components/logo.html +++ b/components/logo.html @@ -15,8 +15,8 @@ - - + +
    diff --git a/components/logos/Badoo.html b/components/logos/Badoo.html index f5cff821d..c2d7894a9 100644 --- a/components/logos/Badoo.html +++ b/components/logos/Badoo.html @@ -15,8 +15,8 @@ - - + +
    diff --git a/components/logos/Grafana.html b/components/logos/Grafana.html index c5d21242f..038790a42 100644 --- a/components/logos/Grafana.html +++ b/components/logos/Grafana.html @@ -15,8 +15,8 @@ - - + +
    diff --git a/components/logos/ManyChat.html b/components/logos/ManyChat.html index 78d9df92b..9a4791605 100644 --- a/components/logos/ManyChat.html +++ b/components/logos/ManyChat.html @@ -15,8 +15,8 @@ - - + +
    diff --git a/components/logos/OpenWeb.html b/components/logos/OpenWeb.html index 79cfd8590..189bec905 100644 --- a/components/logos/OpenWeb.html +++ b/components/logos/OpenWeb.html @@ -15,8 +15,8 @@ - - + +
    diff --git a/docs/3/attributions.html b/docs/3/attributions.html index ac543312e..b14cda45e 100644 --- a/docs/3/attributions.html +++ b/docs/3/attributions.html @@ -15,8 +15,8 @@ - - + +

    Attributions

    Landing Page Images

    diff --git a/docs/3/ecosystem/centrifuge.html b/docs/3/ecosystem/centrifuge.html index 6a0b47dea..5318bf8d5 100644 --- a/docs/3/ecosystem/centrifuge.html +++ b/docs/3/ecosystem/centrifuge.html @@ -15,8 +15,8 @@ - - + +

    Centrifuge library

    Centrifugo is a server built on top of Centrifuge library for Go language.

    diff --git a/docs/3/ecosystem/integrations.html b/docs/3/ecosystem/integrations.html index 23e7022b5..adb8316f1 100644 --- a/docs/3/ecosystem/integrations.html +++ b/docs/3/ecosystem/integrations.html @@ -15,8 +15,8 @@ - - + +

    Framework integrations

    tip

    In general, integrating Centrifugo can be done in several steps even without third-party libraries – see our integration guide. But there are some community-driven projects that provide integration for more native experience or even some additional functionality on top of Centrifugo.

    diff --git a/docs/3/faq.html b/docs/3/faq.html index 44bbd0813..e95ff368c 100644 --- a/docs/3/faq.html +++ b/docs/3/faq.html @@ -15,8 +15,8 @@ - - + +

    Frequently Asked Questions

    Answers to popular questions here.

    diff --git a/docs/3/flow_diagrams.html b/docs/3/flow_diagrams.html index 04eaf67c2..0a20eb1c9 100644 --- a/docs/3/flow_diagrams.html +++ b/docs/3/flow_diagrams.html @@ -15,8 +15,8 @@ - - + +

    flow_diagrams

    For swimlines.io:

    diff --git a/docs/3/getting-started/client_api.html b/docs/3/getting-started/client_api.html index 05e09efad..49b01a31e 100644 --- a/docs/3/getting-started/client_api.html +++ b/docs/3/getting-started/client_api.html @@ -15,8 +15,8 @@ - - + +

    Client API showcase

    This chapter showcases Centrifugo bidirectional client API capabilities – i.e. real-time messaging primitives available on a front-end (can be a browser or a mobile device).

    diff --git a/docs/3/getting-started/design.html b/docs/3/getting-started/design.html index e18a0576a..2d7f08bdc 100644 --- a/docs/3/getting-started/design.html +++ b/docs/3/getting-started/design.html @@ -15,8 +15,8 @@ - - + +

    Design overview

    Let's discuss some architectural and design topics about Centrifugo.

    diff --git a/docs/3/getting-started/highlights.html b/docs/3/getting-started/highlights.html index 22b2ff9a6..b362576ec 100644 --- a/docs/3/getting-started/highlights.html +++ b/docs/3/getting-started/highlights.html @@ -15,8 +15,8 @@ - - + +

    Main highlights

    At this point you know how to build the simplest real-time app with Centrifugo. Beyond the core PUB/SUB functionality Centrifugo provides more features and primitives to build scalable real-time applications. Let's summarize main Centrifugo ✨highlights✨ here. Every point is then extended throughout the documentation.

    diff --git a/docs/3/getting-started/installation.html b/docs/3/getting-started/installation.html index 6c43c35ff..c86a19f50 100644 --- a/docs/3/getting-started/installation.html +++ b/docs/3/getting-started/installation.html @@ -15,8 +15,8 @@ - - + +

    Install Centrifugo

    Centrifugo server is written in Go language. It's an open-source software, the source code is available on Github.

    diff --git a/docs/3/getting-started/integration.html b/docs/3/getting-started/integration.html index 0816c5b92..21f910923 100644 --- a/docs/3/getting-started/integration.html +++ b/docs/3/getting-started/integration.html @@ -15,8 +15,8 @@ - - + +

    Integration guide

    This chapter aims to help you get started with Centrifugo. We will look at a step-by-step workflow of integrating your application with Centrifugo providing links to relevant parts of this documentation.

    diff --git a/docs/3/getting-started/introduction.html b/docs/3/getting-started/introduction.html index 0fb18927f..988b98d8a 100644 --- a/docs/3/getting-started/introduction.html +++ b/docs/3/getting-started/introduction.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo introduction

    Centrifugo is an open-source scalable real-time messaging server in a language-agnostic way.

    diff --git a/docs/3/getting-started/migration_v3.html b/docs/3/getting-started/migration_v3.html index eca0cad2e..c6567ac38 100644 --- a/docs/3/getting-started/migration_v3.html +++ b/docs/3/getting-started/migration_v3.html @@ -15,8 +15,8 @@ - - + +

    Migrating to v3

    This chapter aims to help developers migrate from Centrifugo v2 to Centrifugo v3. Migration should mostly affect the backend part only, so you won't need to change the code of your frontend applications at all. In most cases, all you should do is adapt Centrifugo configuration to match v3 changes and redeploy Centrifugo using v3 build instead of v2.

    diff --git a/docs/3/getting-started/quickstart.html b/docs/3/getting-started/quickstart.html index 34899f20a..450cfa1a3 100644 --- a/docs/3/getting-started/quickstart.html +++ b/docs/3/getting-started/quickstart.html @@ -15,8 +15,8 @@ - - + +

    Quickstart tutorial ⏱️

    Here we will build a very simple browser application with Centrifugo. It works in a way that users connect to Centrifugo over WebSocket, subscribe to a channel, and start receiving all messages published to that channel. In our case, we will send a counter value to all channel subscribers to update it in all open browser tabs in real-time.

    diff --git a/docs/3/pro/analytics.html b/docs/3/pro/analytics.html index bd675ce23..9c9c792c4 100644 --- a/docs/3/pro/analytics.html +++ b/docs/3/pro/analytics.html @@ -15,8 +15,8 @@ - - + +

    Analytics with ClickHouse

    This feature allows exporting information about connections, subscriptions and client operations to ClickHouse thus providing an integration with a real-time (with seconds delay) analytics storage. ClickHouse is super fast and simple to operate with, and it allows effective data keeping for a window of time.

    diff --git a/docs/3/pro/db_namespaces.html b/docs/3/pro/db_namespaces.html index 539006dd4..dc393f295 100644 --- a/docs/3/pro/db_namespaces.html +++ b/docs/3/pro/db_namespaces.html @@ -15,8 +15,8 @@ - - + +

    Database-driven namespace configuration

    Centrifugo PRO supports database-driven namespace configuration. This means that instead of configuring namespaces in a configuration file you will be able to configure them in admin web UI. It's also possible to select a namespace for automatic personal channel subscription.

    diff --git a/docs/3/pro/install_and_run.html b/docs/3/pro/install_and_run.html index 7e07b269b..a33c0e26f 100644 --- a/docs/3/pro/install_and_run.html +++ b/docs/3/pro/install_and_run.html @@ -15,8 +15,8 @@ - - + +

    Install and run PRO version

    caution

    Centrifugo PRO is distributed under commercial license which is different from OSS version. By downloading Centrifugo PRO you automatically accept license terms.

    diff --git a/docs/3/pro/overview.html b/docs/3/pro/overview.html index b94cda2a5..8fc008e01 100644 --- a/docs/3/pro/overview.html +++ b/docs/3/pro/overview.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo PRO overview

    Centrifugo PRO is an extended version of Centrifugo with a set of additional features. These features can provide your business with unique benefits – drastically save development time, reduce resource usage on a server, protect your backend from misusing, and put the system observability to the next level.

    diff --git a/docs/3/pro/performance.html b/docs/3/pro/performance.html index b9d8fd941..d521375c7 100644 --- a/docs/3/pro/performance.html +++ b/docs/3/pro/performance.html @@ -15,8 +15,8 @@ - - + +

    Faster performance

    Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in application, increase server throughput and/or reduce CPU usage on server machines.

    diff --git a/docs/3/pro/process_stats.html b/docs/3/pro/process_stats.html index ec0cbaee6..eaa3bc231 100644 --- a/docs/3/pro/process_stats.html +++ b/docs/3/pro/process_stats.html @@ -15,8 +15,8 @@ - - + +

    CPU and RSS stats

    A useful addition of Centrifugo PRO is an ability to show CPU and RSS memory usage of each node in admin web UI.

    diff --git a/docs/3/pro/singleflight.html b/docs/3/pro/singleflight.html index 2082b47a6..1b5fcbd9c 100644 --- a/docs/3/pro/singleflight.html +++ b/docs/3/pro/singleflight.html @@ -15,8 +15,8 @@ - - + +

    Singleflight

    Centrifugo PRO provides an additional boolean option use_singleflight (default false). When this option enabled Centrifugo will automatically try to merge identical requests to history, online presence or presence stats issued at the same time into one real network request.

    diff --git a/docs/3/pro/throttling.html b/docs/3/pro/throttling.html index 5a9b7bd81..37eccdbc9 100644 --- a/docs/3/pro/throttling.html +++ b/docs/3/pro/throttling.html @@ -15,8 +15,8 @@ - - + +

    Operation throttling

    The throttling feature allows limiting the number of operations each user can issue during a configured time interval. This is useful to protect the system from misusing or protect it from a bug in the application frontend code.

    diff --git a/docs/3/pro/token_revocation.html b/docs/3/pro/token_revocation.html index 721c2a507..666474ea5 100644 --- a/docs/3/pro/token_revocation.html +++ b/docs/3/pro/token_revocation.html @@ -15,8 +15,8 @@ - - + +

    Token revocation API

    One more protective instrument in Centrifugo PRO is API to manage token revocations.

    diff --git a/docs/3/pro/tracing.html b/docs/3/pro/tracing.html index 8ed6764e2..4fe49e803 100644 --- a/docs/3/pro/tracing.html +++ b/docs/3/pro/tracing.html @@ -15,8 +15,8 @@ - - + +

    User and channel tracing

    That's a unique thing. The tracing feature of Centrifugo PRO allows attaching to any channel to see all messages flying towards subscribers or attach to a specific user ID to see all user-related events in real-time.

    diff --git a/docs/3/pro/user_block.html b/docs/3/pro/user_block.html index 31494e163..0e6d39281 100644 --- a/docs/3/pro/user_block.html +++ b/docs/3/pro/user_block.html @@ -15,8 +15,8 @@ - - + +

    User blocking API

    In addition to detailed observability properties Centrifugo PRO provides instruments for performing protective actions. One of such instruments is user blocking API which allows blocking a specific user in Centrifugo.

    diff --git a/docs/3/pro/user_connections.html b/docs/3/pro/user_connections.html index 10e2d052d..fbe1a7ca7 100644 --- a/docs/3/pro/user_connections.html +++ b/docs/3/pro/user_connections.html @@ -15,8 +15,8 @@ - - + +

    User connections API

    Centrifugo PRO provides an additional API call user_connections. It allows getting all active sessions of the user (by user ID) without turning on presence feature for channels at all. It's also possible to attach any JSON payload to a connection which will be then visible in the result of user_connections call. The important thing is that this additional meta information won't be exposed to a client-side (unlike connection info for example).

    diff --git a/docs/3/pro/user_status.html b/docs/3/pro/user_status.html index 937fd5eec..3472b813f 100644 --- a/docs/3/pro/user_status.html +++ b/docs/3/pro/user_status.html @@ -15,8 +15,8 @@ - - + +

    User status

    Centrifugo OSS provides a presence feature for channels. It works well (for channels with reasonably small number of active subscribers though), but sometimes you may need a bit different functionality.

    diff --git a/docs/3/server/admin_web.html b/docs/3/server/admin_web.html index de837e825..2971f2203 100644 --- a/docs/3/server/admin_web.html +++ b/docs/3/server/admin_web.html @@ -15,8 +15,8 @@ - - + +

    Admin web UI

    Centrifugo comes with builtin admin web interface. It can:

    diff --git a/docs/3/server/authentication.html b/docs/3/server/authentication.html index ae18afecc..636922df0 100644 --- a/docs/3/server/authentication.html +++ b/docs/3/server/authentication.html @@ -15,8 +15,8 @@ - - + +

    Client authentication

    To authenticate incoming connection (client) Centrifugo can use JSON Web Token (JWT) passed from the client-side. This way Centrifugo may know the ID of user in your application, also application can pass additional data to Centrifugo inside JWT claims. This chapter describes this authentication mechanism.

    diff --git a/docs/3/server/channels.html b/docs/3/server/channels.html index b731abc5d..e372733ba 100644 --- a/docs/3/server/channels.html +++ b/docs/3/server/channels.html @@ -15,8 +15,8 @@ - - + +

    Channels

    Channel is a route for publications. Clients can be subscribed to a channel to receive real-time messages published to a channel – new publications and join/leave events (if enabled for a channel namespace). A channel subscriber can also ask for a channel presence or channel history information (if enabled for a channel namespace).

    diff --git a/docs/3/server/codes.html b/docs/3/server/codes.html index ec40ee59b..e47617ab3 100644 --- a/docs/3/server/codes.html +++ b/docs/3/server/codes.html @@ -15,8 +15,8 @@ - - + +

    Error and disconnect codes

    This chapter describes error and disconnect codes Centrifugo uses in a client protocol, also error codes which a server API can return in response.

    diff --git a/docs/3/server/configuration.html b/docs/3/server/configuration.html index fd6a4fa1a..edf086a7c 100644 --- a/docs/3/server/configuration.html +++ b/docs/3/server/configuration.html @@ -15,8 +15,8 @@ - - + +

    Configure Centrifugo

    Let's look at how Centrifugo can be configured.

    diff --git a/docs/3/server/console_commands.html b/docs/3/server/console_commands.html index 72176c3b2..89242808f 100644 --- a/docs/3/server/console_commands.html +++ b/docs/3/server/console_commands.html @@ -15,8 +15,8 @@ - - + +

    Console commands

    Here is a list of console commands that come with Centrifugo executable.

    diff --git a/docs/3/server/engines.html b/docs/3/server/engines.html index b145199fd..03724fa3f 100644 --- a/docs/3/server/engines.html +++ b/docs/3/server/engines.html @@ -15,8 +15,8 @@ - - + +

    Engines, scalability

    The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data.

    diff --git a/docs/3/server/history_and_recovery.html b/docs/3/server/history_and_recovery.html index 8b5709e2a..f5c124209 100644 --- a/docs/3/server/history_and_recovery.html +++ b/docs/3/server/history_and_recovery.html @@ -15,8 +15,8 @@ - - + +

    History and recovery

    Centrifugo engines can maintain publication history for channels with configured history size and TTL.

    diff --git a/docs/3/server/infra_tuning.html b/docs/3/server/infra_tuning.html index 4995bc246..15cce7166 100644 --- a/docs/3/server/infra_tuning.html +++ b/docs/3/server/infra_tuning.html @@ -15,8 +15,8 @@ - - + +

    Infrastructure tuning

    As Centrifugo deals with lots of persistent connections your operating system and server infrastructure must be ready for it.

    diff --git a/docs/3/server/load_balancing.html b/docs/3/server/load_balancing.html index 48f9f1d14..bd468b1d1 100644 --- a/docs/3/server/load_balancing.html +++ b/docs/3/server/load_balancing.html @@ -15,8 +15,8 @@ - - + +

    Load balancing

    This chapter shows how to deal with persistent connection load balancing.

    diff --git a/docs/3/server/monitoring.html b/docs/3/server/monitoring.html index 2b35bc89f..1420fe421 100644 --- a/docs/3/server/monitoring.html +++ b/docs/3/server/monitoring.html @@ -15,8 +15,8 @@ - - + +

    Monitoring

    Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.

    diff --git a/docs/3/server/private_channels.html b/docs/3/server/private_channels.html index e16ac8850..29c2caa27 100644 --- a/docs/3/server/private_channels.html +++ b/docs/3/server/private_channels.html @@ -15,8 +15,8 @@ - - + +

    Private channels

    In the channels chapter we mentioned private channels. This chapter has more information about the private channel mechanism in Centrifugo.

    diff --git a/docs/3/server/proxy.html b/docs/3/server/proxy.html index 24b759f50..559d1bad2 100644 --- a/docs/3/server/proxy.html +++ b/docs/3/server/proxy.html @@ -15,8 +15,8 @@ - - + +

    Proxy to backend

    It's possible to proxy some client connection events from Centrifugo to the application backend and react to them in a custom way. For example, it's possible to authenticate connection via request from Centrifugo to application backend, refresh client sessions and answer to RPC calls sent by a client over bidirectional connection.

    diff --git a/docs/3/server/server_api.html b/docs/3/server/server_api.html index 107e797b7..262499bd8 100644 --- a/docs/3/server/server_api.html +++ b/docs/3/server/server_api.html @@ -15,8 +15,8 @@ - - + +

    Server API

    Server API is a way to send various commands to Centrifugo. For example, server API allows publishing messages to channels, get server statistics, etc. There are two kinds of API available at the moment:

    diff --git a/docs/3/server/server_subs.html b/docs/3/server/server_subs.html index 7d8c3517e..c58694c02 100644 --- a/docs/3/server/server_subs.html +++ b/docs/3/server/server_subs.html @@ -15,8 +15,8 @@ - - + +

    Server-side subscriptions

    Centrifugo clients can initiate a subscription to a channel by calling the Subscribe method of client protocol. In most cases, this is the most flexible approach since a client-side usually knows which channels it needs to consume at a concrete moment. But in some situations, all you need is to subscribe your connections to several channels on a server-side at the moment of connection establishment. So client effectively starts receiving publications from those channels without calling the Subscribe method at all.

    diff --git a/docs/3/server/tls.html b/docs/3/server/tls.html index 40eacac24..d2678ff02 100644 --- a/docs/3/server/tls.html +++ b/docs/3/server/tls.html @@ -15,8 +15,8 @@ - - + +

    Configure TLS

    TLS/SSL layer is very important not only for securing your connections but also to increase a diff --git a/docs/3/transports/client_protocol.html b/docs/3/transports/client_protocol.html index 534e24ea3..4f4769a1e 100644 --- a/docs/3/transports/client_protocol.html +++ b/docs/3/transports/client_protocol.html @@ -15,8 +15,8 @@ - - + +

    Client protocol

    This chapter describes internal bidirectional client-server protocol in details to help developers build new client libraries or understand how existing client connectors work.

    diff --git a/docs/3/transports/client_sdk.html b/docs/3/transports/client_sdk.html index 857769a00..d915758a7 100644 --- a/docs/3/transports/client_sdk.html +++ b/docs/3/transports/client_sdk.html @@ -15,8 +15,8 @@ - - + +

    Client real-time SDKs

    The following SDKs allow connecting to Centrifugo from the application frontend:

    diff --git a/docs/3/transports/overview.html b/docs/3/transports/overview.html index 0fa3a4f59..74a958710 100644 --- a/docs/3/transports/overview.html +++ b/docs/3/transports/overview.html @@ -15,8 +15,8 @@ - - + +

    Real-time transports

    Centrifugo supports a variety of transports to deliver real-time messages to clients.

    diff --git a/docs/3/transports/sockjs.html b/docs/3/transports/sockjs.html index 0ed73cced..1a49b5fe6 100644 --- a/docs/3/transports/sockjs.html +++ b/docs/3/transports/sockjs.html @@ -15,8 +15,8 @@ - - + +

    SockJS

    SockJS is a polyfill browser library which provides HTTP-based fallback transports in case when it's not possible to establish Websocket connection. This can happen in old client browsers or because of some proxy behind client and server that cuts of Websocket traffic. You can find more information on SockJS project Github page.

    diff --git a/docs/3/transports/uni_grpc.html b/docs/3/transports/uni_grpc.html index 58816290b..160917726 100644 --- a/docs/3/transports/uni_grpc.html +++ b/docs/3/transports/uni_grpc.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional GRPC

    It's possible to connect to GRPC unidirectional stream to consume real-time messages from Centrifugo. In this case you need to generate GRPC code for your language on client-side.

    diff --git a/docs/3/transports/uni_http_stream.html b/docs/3/transports/uni_http_stream.html index 3a5a33c2e..811f2111a 100644 --- a/docs/3/transports/uni_http_stream.html +++ b/docs/3/transports/uni_http_stream.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional HTTP streaming

    Default unidirectional HTTP streaming connection endpoint in Centrifugo is:

    diff --git a/docs/3/transports/uni_sse.html b/docs/3/transports/uni_sse.html index 643f299c9..df22c88ef 100644 --- a/docs/3/transports/uni_sse.html +++ b/docs/3/transports/uni_sse.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional SSE (EventSource)

    Default unidirectional SSE (EventSource) connection endpoint in Centrifugo is:

    diff --git a/docs/3/transports/uni_websocket.html b/docs/3/transports/uni_websocket.html index 10fc16e6d..aa58a9299 100644 --- a/docs/3/transports/uni_websocket.html +++ b/docs/3/transports/uni_websocket.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional WebSocket

    Default unidirectional WebSocket connection endpoint in Centrifugo is:

    diff --git a/docs/3/transports/websocket.html b/docs/3/transports/websocket.html index 2c32cb564..7bb25572c 100644 --- a/docs/3/transports/websocket.html +++ b/docs/3/transports/websocket.html @@ -15,8 +15,8 @@ - - + +

    WebSocket

    Websocket is the main transport in Centrifugo. It's a very efficient low-overhead protocol on top of TCP.

    diff --git a/docs/4/attributions.html b/docs/4/attributions.html index 40239e5ec..89c9e5289 100644 --- a/docs/4/attributions.html +++ b/docs/4/attributions.html @@ -15,8 +15,8 @@ - - + +

    Attributions

    Landing Page Images

    diff --git a/docs/4/faq.html b/docs/4/faq.html index 113908817..10de02e88 100644 --- a/docs/4/faq.html +++ b/docs/4/faq.html @@ -15,8 +15,8 @@ - - + +

    Frequently Asked Questions

    Answers to popular questions here.

    diff --git a/docs/4/flow_diagrams.html b/docs/4/flow_diagrams.html index e76b592d2..c6e9b6bc9 100644 --- a/docs/4/flow_diagrams.html +++ b/docs/4/flow_diagrams.html @@ -15,8 +15,8 @@ - - + +

    flow_diagrams

    For swimlanes.io:

    diff --git a/docs/4/getting-started/client_api.html b/docs/4/getting-started/client_api.html index 4531269b8..858237b61 100644 --- a/docs/4/getting-started/client_api.html +++ b/docs/4/getting-started/client_api.html @@ -15,8 +15,8 @@ - - + +

    Client API showcase

    This chapter showcases Centrifugo bidirectional client API capabilities – i.e. real-time messaging primitives available on a front-end (can be a browser or a mobile device).

    diff --git a/docs/4/getting-started/community.html b/docs/4/getting-started/community.html index 1bdff1b41..f267dcba4 100644 --- a/docs/4/getting-started/community.html +++ b/docs/4/getting-started/community.html @@ -15,8 +15,8 @@ - - + +

    Join community

    If you find Centrifugo interesting – please welcome to our community rooms in Telegram (the most active) and Discord:

    diff --git a/docs/4/getting-started/design.html b/docs/4/getting-started/design.html index b4831ecf6..61a376371 100644 --- a/docs/4/getting-started/design.html +++ b/docs/4/getting-started/design.html @@ -15,8 +15,8 @@ - - + +

    Design overview

    Let's discuss some architectural and design topics about Centrifugo.

    diff --git a/docs/4/getting-started/ecosystem.html b/docs/4/getting-started/ecosystem.html index 5e6cb4d2b..d17aaea01 100644 --- a/docs/4/getting-started/ecosystem.html +++ b/docs/4/getting-started/ecosystem.html @@ -15,8 +15,8 @@ - - + +

    Ecosystem notes

    Some additional notes about our ecosystem which may help you develop with our tech.

    diff --git a/docs/4/getting-started/highlights.html b/docs/4/getting-started/highlights.html index e8317f7bd..f9bff8ccf 100644 --- a/docs/4/getting-started/highlights.html +++ b/docs/4/getting-started/highlights.html @@ -15,8 +15,8 @@ - - + +

    Main highlights

    At this point you know how to build the simplest real-time app with Centrifugo. Beyond the core PUB/SUB functionality Centrifugo provides more features and primitives to build scalable real-time applications. Let's summarize main Centrifugo ✨highlights✨ here. Every point is then extended throughout the documentation.

    diff --git a/docs/4/getting-started/installation.html b/docs/4/getting-started/installation.html index c9cbed995..d89d6446e 100644 --- a/docs/4/getting-started/installation.html +++ b/docs/4/getting-started/installation.html @@ -15,8 +15,8 @@ - - + +

    Install Centrifugo

    Centrifugo server is written in Go language. It's an open-source software, the source code is available on Github.

    diff --git a/docs/4/getting-started/integration.html b/docs/4/getting-started/integration.html index 55c575c65..aacfddd25 100644 --- a/docs/4/getting-started/integration.html +++ b/docs/4/getting-started/integration.html @@ -15,8 +15,8 @@ - - + +

    Integration guide

    This chapter aims to help you get started with Centrifugo. We will look at a step-by-step workflow of integrating your application with Centrifugo providing links to relevant parts of this documentation.

    diff --git a/docs/4/getting-started/introduction.html b/docs/4/getting-started/introduction.html index 8687a79da..fcb12d221 100644 --- a/docs/4/getting-started/introduction.html +++ b/docs/4/getting-started/introduction.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo introduction

    diff --git a/docs/4/getting-started/migration_v4.html b/docs/4/getting-started/migration_v4.html index 78a79f176..6cd3c6df8 100644 --- a/docs/4/getting-started/migration_v4.html +++ b/docs/4/getting-started/migration_v4.html @@ -15,8 +15,8 @@ - - + +

    Migrating to v4

    Centrifugo v4 development was concentrated around two main things:

    diff --git a/docs/4/getting-started/quickstart.html b/docs/4/getting-started/quickstart.html index 6749aa8d3..3134b10b1 100644 --- a/docs/4/getting-started/quickstart.html +++ b/docs/4/getting-started/quickstart.html @@ -15,8 +15,8 @@ - - + +

    Quickstart tutorial ⏱️

    In this tutorial we will build a very simple browser application with Centrifugo. Users will connect to Centrifugo over WebSocket, subscribe to a channel, and start receiving all channel publications (messages published to that channel). In our case, we will send a counter value to all channel subscribers to update counter widget in all open browser tabs in real-time.

    diff --git a/docs/4/pro/analytics.html b/docs/4/pro/analytics.html index 34a21b4ee..e45b4954c 100644 --- a/docs/4/pro/analytics.html +++ b/docs/4/pro/analytics.html @@ -15,8 +15,8 @@ - - + +

    Analytics with ClickHouse

    This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ClickHouse thus providing an integration with a real-time (with seconds delay) analytics storage. ClickHouse is super fast for analytical queries, simple to operate with and it allows effective data keeping for a window of time. Also, it's relatively simple to create a high performance ClickHouse cluster.

    diff --git a/docs/4/pro/capabilities.html b/docs/4/pro/capabilities.html index 677464b92..4360f7e69 100644 --- a/docs/4/pro/capabilities.html +++ b/docs/4/pro/capabilities.html @@ -15,8 +15,8 @@ - - + +

    Channel capabilities

    At this point you know that Centrifugo allows configuring channel permissions on a per-namespace level. When creating a new real-time feature it's recommended to create a new namespace for it and configure permissions. To achieve a better channel permission control inside a namespace Centrifugo PRO provides possibility to set capabilities on individual connection basis, or individual channel subscription basis.

    diff --git a/docs/4/pro/cel_expressions.html b/docs/4/pro/cel_expressions.html index fa7342875..682a53e38 100644 --- a/docs/4/pro/cel_expressions.html +++ b/docs/4/pro/cel_expressions.html @@ -15,8 +15,8 @@ - - + +

    CEL expressions

    This PRO feature is under active development, some changes expected here 🚧

    diff --git a/docs/4/pro/channel_patterns.html b/docs/4/pro/channel_patterns.html index 4c5f9295e..defd0d677 100644 --- a/docs/4/pro/channel_patterns.html +++ b/docs/4/pro/channel_patterns.html @@ -15,8 +15,8 @@ - - + +

    Channel patterns

    This PRO feature is under active development, some changes expected here 🚧

    diff --git a/docs/4/pro/client_message_batching.html b/docs/4/pro/client_message_batching.html index 52403de95..6e0a22b18 100644 --- a/docs/4/pro/client_message_batching.html +++ b/docs/4/pro/client_message_batching.html @@ -15,8 +15,8 @@ - - + +

    Message batching control

    This PRO feature is under active development, some changes expected here 🚧

    diff --git a/docs/4/pro/connections.html b/docs/4/pro/connections.html index 723dbfb56..3545639d3 100644 --- a/docs/4/pro/connections.html +++ b/docs/4/pro/connections.html @@ -15,8 +15,8 @@ - - + +

    Connections API

    Centrifugo PRO offers an extra API call, connections, which enables retrieval of all active sessions (based on user ID or expression) without the need to activate the presence feature for channels. Furthermore, developers can attach any desired JSON payload to a connection that will then be visible in the result of the connections call. It's worth noting that this additional meta-information remains hidden from the client-side, unlike the info associated with the connection.

    diff --git a/docs/4/pro/install_and_run.html b/docs/4/pro/install_and_run.html index d55e63bc4..d3976b542 100644 --- a/docs/4/pro/install_and_run.html +++ b/docs/4/pro/install_and_run.html @@ -15,8 +15,8 @@ - - + +

    Install and run PRO version

    Centrifugo PRO license agreement

    Centrifugo PRO is distributed by Centrifugal Labs LTD under commercial license which is different from OSS version. By downloading Centrifugo PRO you automatically accept commercial license terms.

    diff --git a/docs/4/pro/overview.html b/docs/4/pro/overview.html index 3c444a7e2..a4a822a9e 100644 --- a/docs/4/pro/overview.html +++ b/docs/4/pro/overview.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo PRO overview

    diff --git a/docs/4/pro/performance.html b/docs/4/pro/performance.html index 30a3f0b8d..a6562c817 100644 --- a/docs/4/pro/performance.html +++ b/docs/4/pro/performance.html @@ -15,8 +15,8 @@ - - + +

    Faster performance

    diff --git a/docs/4/pro/process_stats.html b/docs/4/pro/process_stats.html index 5d0098a0c..6da64615e 100644 --- a/docs/4/pro/process_stats.html +++ b/docs/4/pro/process_stats.html @@ -15,8 +15,8 @@ - - + +

    CPU and RSS stats

    A useful addition of Centrifugo PRO is an ability to show CPU and RSS memory usage of each node in admin web UI.

    diff --git a/docs/4/pro/push_notifications.html b/docs/4/pro/push_notifications.html index b3feab8a5..4a73e4db4 100644 --- a/docs/4/pro/push_notifications.html +++ b/docs/4/pro/push_notifications.html @@ -15,8 +15,8 @@ - - + +

    Push notification API

    This PRO feature is under active development, some changes expected here 🚧

    diff --git a/docs/4/pro/singleflight.html b/docs/4/pro/singleflight.html index 9e376b7b6..701aa1316 100644 --- a/docs/4/pro/singleflight.html +++ b/docs/4/pro/singleflight.html @@ -15,8 +15,8 @@ - - + +

    Singleflight

    Centrifugo PRO provides an additional boolean option use_singleflight (default false). When this option enabled Centrifugo will automatically try to merge identical requests to history, online presence or presence stats issued at the same time into one real network request. It will do this by using in-memory component called singleflight.

    diff --git a/docs/4/pro/throttling.html b/docs/4/pro/throttling.html index efb329fa3..07577e718 100644 --- a/docs/4/pro/throttling.html +++ b/docs/4/pro/throttling.html @@ -15,8 +15,8 @@ - - + +

    Operation throttling

    The throttling feature allows limiting the number of operations each connection or user can issue during a configured time interval. This is useful to protect the system from misusing, detecting and disconnecting abusive or broken (due to the bug in the frontend application) clients which add unwanted load on a server.

    diff --git a/docs/4/pro/token_revocation.html b/docs/4/pro/token_revocation.html index c5b853bb6..e08323446 100644 --- a/docs/4/pro/token_revocation.html +++ b/docs/4/pro/token_revocation.html @@ -15,8 +15,8 @@ - - + +

    Token revocation API

    One more protective instrument in Centrifugo PRO is API to manage token revocations.

    diff --git a/docs/4/pro/tracing.html b/docs/4/pro/tracing.html index c8fa34648..ef97ddfbd 100644 --- a/docs/4/pro/tracing.html +++ b/docs/4/pro/tracing.html @@ -15,8 +15,8 @@ - - + +

    User and channel tracing

    That's a unique thing. The tracing feature of Centrifugo PRO allows attaching to any channel to see all messages flying towards subscribers or attach to a specific user ID to see all user-related events in real-time.

    diff --git a/docs/4/pro/user_block.html b/docs/4/pro/user_block.html index 5495b6945..cb4249faa 100644 --- a/docs/4/pro/user_block.html +++ b/docs/4/pro/user_block.html @@ -15,8 +15,8 @@ - - + +

    User blocking API

    One additional instrument for making protective actions in Centrifugo PRO is user blocking API which allows blocking a specific user on Centrifugo level.

    diff --git a/docs/4/pro/user_status.html b/docs/4/pro/user_status.html index c0e8dd0e2..35f691dc4 100644 --- a/docs/4/pro/user_status.html +++ b/docs/4/pro/user_status.html @@ -15,8 +15,8 @@ - - + +

    User status API

    Centrifugo OSS provides a presence feature for channels. It works well (for channels with reasonably small number of active subscribers though), but sometimes you may need a bit different functionality.

    diff --git a/docs/4/server/admin_web.html b/docs/4/server/admin_web.html index 071b40af8..bdeab0b2f 100644 --- a/docs/4/server/admin_web.html +++ b/docs/4/server/admin_web.html @@ -15,8 +15,8 @@ - - + +

    Admin web UI

    Centrifugo comes with a built-in admin web interface. It can:

    diff --git a/docs/4/server/authentication.html b/docs/4/server/authentication.html index 3ec4de8de..fc341dbb3 100644 --- a/docs/4/server/authentication.html +++ b/docs/4/server/authentication.html @@ -15,8 +15,8 @@ - - + +

    Client JWT authentication

    To authenticate incoming connection (client) Centrifugo can use JSON Web Token (JWT) passed from the client-side. This way Centrifugo may know the ID of user in your application, also application can pass additional data to Centrifugo inside JWT claims. This chapter describes this authentication mechanism.

    diff --git a/docs/4/server/channel_permissions.html b/docs/4/server/channel_permissions.html index 9eae257f5..76595ccb2 100644 --- a/docs/4/server/channel_permissions.html +++ b/docs/4/server/channel_permissions.html @@ -15,8 +15,8 @@ - - + +

    Channel permission model

    When using Centrifugo server API you don't need to think about channel permissions at all – everything is allowed. In server API case, request to Centrifugo must be issued by your application backend – so you have all the power to check any required permissions before issuing API request to Centrifugo.

    diff --git a/docs/4/server/channel_token_auth.html b/docs/4/server/channel_token_auth.html index e9136db0a..a8d42c067 100644 --- a/docs/4/server/channel_token_auth.html +++ b/docs/4/server/channel_token_auth.html @@ -15,8 +15,8 @@ - - + +

    Channel JWT authorization

    In the chapter about channel permissions we mentioned that to subscribe on a channel client can provide subscription token. This chapter has more information about the subscription token mechanism in Centrifugo.

    diff --git a/docs/4/server/channels.html b/docs/4/server/channels.html index 16b64ab8a..0db2802b0 100644 --- a/docs/4/server/channels.html +++ b/docs/4/server/channels.html @@ -15,8 +15,8 @@ - - + +

    Channels and namespaces

    Upon connecting to a server clients can subscribe to channels. Channel is one of the core concepts of Centrifugo. Most of the time when integrating Centrifugo you will work with channels and decide what is the best channel configuration for your application.

    diff --git a/docs/4/server/codes.html b/docs/4/server/codes.html index 63141b7bd..817cddde1 100644 --- a/docs/4/server/codes.html +++ b/docs/4/server/codes.html @@ -15,8 +15,8 @@ - - + +

    Error and disconnect codes

    This chapter describes error and disconnect codes Centrifugo uses in a client protocol, also error codes which a server API can return in response.

    diff --git a/docs/4/server/configuration.html b/docs/4/server/configuration.html index 5b15e98cb..6e91b4ba0 100644 --- a/docs/4/server/configuration.html +++ b/docs/4/server/configuration.html @@ -15,8 +15,8 @@ - - + +

    Configure Centrifugo

    Let's look at how Centrifugo can be configured.

    diff --git a/docs/4/server/console_commands.html b/docs/4/server/console_commands.html index 6303134fa..40af99fa7 100644 --- a/docs/4/server/console_commands.html +++ b/docs/4/server/console_commands.html @@ -15,8 +15,8 @@ - - + +

    Helper CLI commands

    Here is a list of helpful command-line commands that come with Centrifugo executable.

    diff --git a/docs/4/server/engines.html b/docs/4/server/engines.html index a6c53ee65..c1956ecb2 100644 --- a/docs/4/server/engines.html +++ b/docs/4/server/engines.html @@ -15,8 +15,8 @@ - - + +

    Engines and scalability

    The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data.

    diff --git a/docs/4/server/history_and_recovery.html b/docs/4/server/history_and_recovery.html index 460407804..966d76a6e 100644 --- a/docs/4/server/history_and_recovery.html +++ b/docs/4/server/history_and_recovery.html @@ -15,8 +15,8 @@ - - + +

    History and recovery

    Centrifugo engines can maintain publication history for channels with configured history size and TTL.

    diff --git a/docs/4/server/infra_tuning.html b/docs/4/server/infra_tuning.html index 017645419..ea406adb4 100644 --- a/docs/4/server/infra_tuning.html +++ b/docs/4/server/infra_tuning.html @@ -15,8 +15,8 @@ - - + +

    Infrastructure tuning

    As Centrifugo deals with lots of persistent connections your operating system and server infrastructure must be ready for it.

    diff --git a/docs/4/server/load_balancing.html b/docs/4/server/load_balancing.html index 19ffecb0d..7f820ad4d 100644 --- a/docs/4/server/load_balancing.html +++ b/docs/4/server/load_balancing.html @@ -15,8 +15,8 @@ - - + +

    Load balancing

    This chapter shows how to deal with persistent connection load balancing.

    diff --git a/docs/4/server/monitoring.html b/docs/4/server/monitoring.html index 8647b0c22..e18c5e90b 100644 --- a/docs/4/server/monitoring.html +++ b/docs/4/server/monitoring.html @@ -15,8 +15,8 @@ - - + +

    Metrics monitoring

    Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.

    diff --git a/docs/4/server/presence.html b/docs/4/server/presence.html index 2dd0a71ce..cddc8d52f 100644 --- a/docs/4/server/presence.html +++ b/docs/4/server/presence.html @@ -15,8 +15,8 @@ - - + +

    Online presence

    The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users in real-time on a specific channel. It provides live data about which users are currently connected to your application.

    diff --git a/docs/4/server/proxy.html b/docs/4/server/proxy.html index 63a331e7d..85e76a9c2 100644 --- a/docs/4/server/proxy.html +++ b/docs/4/server/proxy.html @@ -15,8 +15,8 @@ - - + +

    Proxy events to the backend

    It's possible to proxy some client connection events from Centrifugo to the application backend and react to them in a custom way. For example, it's possible to authenticate connection via request from Centrifugo to application backend, refresh client sessions and answer to RPC calls sent by a client over bidirectional connection. Also, you may control subscription and publication permissions using these hooks.

    diff --git a/docs/4/server/server_api.html b/docs/4/server/server_api.html index 4b276ee46..ca4cfb44e 100644 --- a/docs/4/server/server_api.html +++ b/docs/4/server/server_api.html @@ -15,8 +15,8 @@ - - + +

    Server API walkthrough

    Server API provides a way to send various commands to Centrifugo. For example, server API allows publishing messages to channels, get server statistics, etc. There are two kinds of API available at the moment:

    diff --git a/docs/4/server/server_subs.html b/docs/4/server/server_subs.html index c217a06d5..d493e7fd3 100644 --- a/docs/4/server/server_subs.html +++ b/docs/4/server/server_subs.html @@ -15,8 +15,8 @@ - - + +

    Server-side subscriptions

    Centrifugo clients can initiate a subscription to a channel by calling the subscribe method of client API. In most cases, client-side subscriptions is a more flexible and recommended approach since a frontend usually knows which channels it needs to consume at a concrete moment.

    diff --git a/docs/4/server/tls.html b/docs/4/server/tls.html index 6fb9a38f3..e9f859c93 100644 --- a/docs/4/server/tls.html +++ b/docs/4/server/tls.html @@ -15,8 +15,8 @@ - - + +

    Configure TLS

    TLS/SSL layer is very important not only for securing your connections but also to increase a diff --git a/docs/4/transports/client_api.html b/docs/4/transports/client_api.html index 2bb1eb0b0..031e5f9dc 100644 --- a/docs/4/transports/client_api.html +++ b/docs/4/transports/client_api.html @@ -15,8 +15,8 @@ - - + +

    Client SDK API

    Centrifugo has several client SDKs to establish a real-time connection with a server. Centrifugo SDKs use WebSocket as the main data transport and send/receive messages encoded according to our bidirectional protocol. That protocol is built on top of the Protobuf schema (both JSON and binary Protobuf formats are supported). It provides asynchronous communication, sending RPC, multiplexing subscriptions to channels, etc. Client SDK wraps the protocol and exposes a set of APIs to developers.

    diff --git a/docs/4/transports/client_protocol.html b/docs/4/transports/client_protocol.html index 852b5bc1a..6e94abb43 100644 --- a/docs/4/transports/client_protocol.html +++ b/docs/4/transports/client_protocol.html @@ -15,8 +15,8 @@ - - + +

    Client protocol

    This chapter describes the core concepts of Centrifugo bidirectional client protocol – concentrating on framing level. If you want to find out details about exposed client API then look at client API document.

    diff --git a/docs/4/transports/client_sdk.html b/docs/4/transports/client_sdk.html index 85618bf34..e2a2d11e3 100644 --- a/docs/4/transports/client_sdk.html +++ b/docs/4/transports/client_sdk.html @@ -15,8 +15,8 @@ - - + +

    Client real-time SDKs

    In the previous chapter we investigated common principles of Centrifugo client SDK API. Here we will provide a list of available bidirectional connectors you can use to communicate with Centrifugo.

    diff --git a/docs/4/transports/http_stream.html b/docs/4/transports/http_stream.html index aa10b7d26..09ee04423 100644 --- a/docs/4/transports/http_stream.html +++ b/docs/4/transports/http_stream.html @@ -15,8 +15,8 @@ - - + +

    HTTP streaming, with bidirectional emulation

    HTTP streaming connection endpoint in Centrifugo is:

    diff --git a/docs/4/transports/overview.html b/docs/4/transports/overview.html index 5aad7eff7..6307c3f82 100644 --- a/docs/4/transports/overview.html +++ b/docs/4/transports/overview.html @@ -15,8 +15,8 @@ - - + +

    Real-time transports

    Centrifugo supports a variety of transports to deliver real-time messages to clients.

    diff --git a/docs/4/transports/sockjs.html b/docs/4/transports/sockjs.html index 59d18fd9c..33a69be2c 100644 --- a/docs/4/transports/sockjs.html +++ b/docs/4/transports/sockjs.html @@ -15,8 +15,8 @@ - - + +

    SockJS

    SockJS is a polyfill browser library which provides HTTP-based fallback transports in case when it's not possible to establish Websocket connection. This can happen in old client browsers or because of some proxy behind client and server that cuts of Websocket traffic. You can find more information on SockJS project Github page.

    diff --git a/docs/4/transports/sse.html b/docs/4/transports/sse.html index a3efba399..77d196ca8 100644 --- a/docs/4/transports/sse.html +++ b/docs/4/transports/sse.html @@ -15,8 +15,8 @@ - - + +

    SSE (EventSource), with bidirectional emulation

    SSE (EventSource) connection endpoint in Centrifugo is:

    diff --git a/docs/4/transports/uni_grpc.html b/docs/4/transports/uni_grpc.html index fd84bcf90..1238cc781 100644 --- a/docs/4/transports/uni_grpc.html +++ b/docs/4/transports/uni_grpc.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional GRPC

    It's possible to connect to GRPC unidirectional stream to consume real-time messages from Centrifugo. In this case you need to generate GRPC code for your language on client-side.

    diff --git a/docs/4/transports/uni_http_stream.html b/docs/4/transports/uni_http_stream.html index 2f8e5a262..c47b83e70 100644 --- a/docs/4/transports/uni_http_stream.html +++ b/docs/4/transports/uni_http_stream.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional HTTP streaming

    Default unidirectional HTTP streaming connection endpoint in Centrifugo is:

    diff --git a/docs/4/transports/uni_sse.html b/docs/4/transports/uni_sse.html index 750ac77f5..89110adc4 100644 --- a/docs/4/transports/uni_sse.html +++ b/docs/4/transports/uni_sse.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional SSE (EventSource)

    Default unidirectional SSE (EventSource) connection endpoint in Centrifugo is:

    diff --git a/docs/4/transports/uni_websocket.html b/docs/4/transports/uni_websocket.html index bfbd745fb..140fd3523 100644 --- a/docs/4/transports/uni_websocket.html +++ b/docs/4/transports/uni_websocket.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional WebSocket

    Default unidirectional WebSocket connection endpoint in Centrifugo is:

    diff --git a/docs/4/transports/websocket.html b/docs/4/transports/websocket.html index 3a611611e..f0b095130 100644 --- a/docs/4/transports/websocket.html +++ b/docs/4/transports/websocket.html @@ -15,8 +15,8 @@ - - + +

    WebSocket

    Websocket is the main transport in Centrifugo. It's a very efficient low-overhead protocol on top of TCP.

    diff --git a/docs/4/transports/webtransport.html b/docs/4/transports/webtransport.html index 7b232f5c9..6590daa8c 100644 --- a/docs/4/transports/webtransport.html +++ b/docs/4/transports/webtransport.html @@ -15,8 +15,8 @@ - - + +

    WebTransport

    WebTransport is an API offering low-latency, bidirectional, client-server messaging on top of HTTP/3. See Using WebTransport article that gives a good overview of it.

    diff --git a/docs/attributions.html b/docs/attributions.html index 3e6a974bc..1ea4b1d83 100644 --- a/docs/attributions.html +++ b/docs/attributions.html @@ -15,8 +15,8 @@ - - + +

    Attributions

    Landing Page Images

    diff --git a/docs/faq.html b/docs/faq.html index 66e4db23e..f353ca124 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -15,8 +15,8 @@ - - + +

    Frequently Asked Questions

    Answers to popular questions here.

    diff --git a/docs/flow_diagrams.html b/docs/flow_diagrams.html index 7db5ab03e..ada093af8 100644 --- a/docs/flow_diagrams.html +++ b/docs/flow_diagrams.html @@ -15,8 +15,8 @@ - - + +

    flow_diagrams

    For swimlanes.io:

    diff --git a/docs/getting-started/community.html b/docs/getting-started/community.html index 37dce7382..a1e7a25a3 100644 --- a/docs/getting-started/community.html +++ b/docs/getting-started/community.html @@ -15,8 +15,8 @@ - - + +

    Join community

    If you find Centrifugo interesting, you are welcome to join our community rooms on Telegram (the most active) and Discord:

    diff --git a/docs/getting-started/comparisons.html b/docs/getting-started/comparisons.html index 496a4866a..db0c32384 100644 --- a/docs/getting-started/comparisons.html +++ b/docs/getting-started/comparisons.html @@ -15,8 +15,8 @@ - - + +

    Comparing with others

    Let's compare Centrifugo with various systems. These comparisons arose from popular questions raised in our communities. Here we are emphasizing things that make Centrifugo special.

    diff --git a/docs/getting-started/design.html b/docs/getting-started/design.html index cf5af5c45..295f2bea7 100644 --- a/docs/getting-started/design.html +++ b/docs/getting-started/design.html @@ -15,8 +15,8 @@ - - + +

    Design overview

    Let's discuss some architectural and design topics about Centrifugo.

    diff --git a/docs/getting-started/ecosystem.html b/docs/getting-started/ecosystem.html index 0b6e21890..e73404c99 100644 --- a/docs/getting-started/ecosystem.html +++ b/docs/getting-started/ecosystem.html @@ -15,8 +15,8 @@ - - + +

    Ecosystem notes

    Some additional notes about our ecosystem which may help you develop with our tech.

    diff --git a/docs/getting-started/highlights.html b/docs/getting-started/highlights.html index b1b1ace44..a430fa28c 100644 --- a/docs/getting-started/highlights.html +++ b/docs/getting-started/highlights.html @@ -15,8 +15,8 @@ - - + +

    Main highlights

    At this point, you know how to build the simplest real-time app with Centrifugo. Beyond the core PUB/SUB functionality, Centrifugo provides more features and primitives to build scalable real-time applications. Let's summarize the main Centrifugo ✨highlights✨ here. Every point is then extended throughout the documentation.

    diff --git a/docs/getting-started/installation.html b/docs/getting-started/installation.html index 569ac35db..05ba0c28f 100644 --- a/docs/getting-started/installation.html +++ b/docs/getting-started/installation.html @@ -15,8 +15,8 @@ - - + +

    Install Centrifugo

    Centrifugo server is written in the Go language. It's open-source software, and the source code is available on Github.

    diff --git a/docs/getting-started/integration.html b/docs/getting-started/integration.html index d82bfc402..7bf8acc3c 100644 --- a/docs/getting-started/integration.html +++ b/docs/getting-started/integration.html @@ -15,8 +15,8 @@ - - + +

    Integration guide

    This chapter aims to help you get started with Centrifugo. We will look at a step-by-step workflow of integrating your application with Centrifugo, providing links to relevant parts of this documentation.

    diff --git a/docs/getting-started/introduction.html b/docs/getting-started/introduction.html index a51f58ab2..3191657bf 100644 --- a/docs/getting-started/introduction.html +++ b/docs/getting-started/introduction.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo introduction

    diff --git a/docs/getting-started/migration_v4.html b/docs/getting-started/migration_v4.html index 0156b6731..aab261727 100644 --- a/docs/getting-started/migration_v4.html +++ b/docs/getting-started/migration_v4.html @@ -15,8 +15,8 @@ - - + +

    Migrating to v4

    Centrifugo v4 development was concentrated around two main things:

    diff --git a/docs/getting-started/migration_v5.html b/docs/getting-started/migration_v5.html index d30b2bd47..6554d2dab 100644 --- a/docs/getting-started/migration_v5.html +++ b/docs/getting-started/migration_v5.html @@ -15,8 +15,8 @@ - - + +

    Migrating to v5

    Centrifugo v5 migration from v4 should be smooth for most of the use cases.

    diff --git a/docs/getting-started/quickstart.html b/docs/getting-started/quickstart.html index 138bfb876..0bd848999 100644 --- a/docs/getting-started/quickstart.html +++ b/docs/getting-started/quickstart.html @@ -15,8 +15,8 @@ - - + +

    Quickstart tutorial ⏱️

    In this tutorial, we will build a very simple browser application with Centrifugo. Users will connect to Centrifugo over WebSocket, subscribe to a channel, and start receiving all channel publications (messages published to that channel). In our case, we will send a counter value to all channel subscribers to update the counter widget in all open browser tabs in real-time.

    diff --git a/docs/pro/admin_idp_auth.html b/docs/pro/admin_idp_auth.html index 30746886d..0e13f194e 100644 --- a/docs/pro/admin_idp_auth.html +++ b/docs/pro/admin_idp_auth.html @@ -15,8 +15,8 @@ - - + +

    SSO for admin UI using OpenID connect (OIDC)

    Admin UI of Centrifugo OSS supports only one admin user identified by the preconfigured password. For the corporate and enterprise environments Centrifugo PRO provides a way to integrate with popular User Identity Providers (IDP), such as Okta, KeyCloak, Google Workspace, Azure and others. Most of the modern providers which support OpenID connect (OIDC) protocol with Proof Key for Code Exchange diff --git a/docs/pro/analytics.html b/docs/pro/analytics.html index 31d7afe74..a967082b3 100644 --- a/docs/pro/analytics.html +++ b/docs/pro/analytics.html @@ -15,8 +15,8 @@ - - + +

    Analytics with ClickHouse

    This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ClickHouse thus providing an integration with a real-time (with seconds delay) analytics storage. ClickHouse is super fast for analytical queries, simple to operate with and it allows effective data keeping for a window of time. Also, it's relatively simple to create a high performance ClickHouse cluster.

    diff --git a/docs/pro/capabilities.html b/docs/pro/capabilities.html index b7cfbad66..d61795d51 100644 --- a/docs/pro/capabilities.html +++ b/docs/pro/capabilities.html @@ -15,8 +15,8 @@ - - + +

    Channel capabilities

    At this point you know that Centrifugo allows configuring channel permissions on a per-namespace level. When creating a new real-time feature it's recommended to create a new namespace for it and configure permissions. To achieve a better channel permission control inside a namespace Centrifugo PRO provides possibility to set capabilities on individual connection basis, or individual channel subscription basis.

    diff --git a/docs/pro/cel_expressions.html b/docs/pro/cel_expressions.html index 4c4cc9227..0ab93c015 100644 --- a/docs/pro/cel_expressions.html +++ b/docs/pro/cel_expressions.html @@ -15,8 +15,8 @@ - - + +

    Channel CEL expressions

    Centrifugo PRO supports CEL expressions (Common Expression Language) for checking channel operation permissions.

    diff --git a/docs/pro/channel_cache_empty.html b/docs/pro/channel_cache_empty.html index 5c9b77e89..66baf1797 100644 --- a/docs/pro/channel_cache_empty.html +++ b/docs/pro/channel_cache_empty.html @@ -15,8 +15,8 @@ - - + +

    Channel cache empty events

    Centrifugo PRO can notify the backend when a client subscribes to the channel using cache recovery mode, but there is no latest publication found in the history stream to load the initial state – i.e. in the case of "cache miss" event. The backend may react to the event and populate the cache by publishing the current state to the channel.

    diff --git a/docs/pro/channel_patterns.html b/docs/pro/channel_patterns.html index 2bece9b80..a2efba7d2 100644 --- a/docs/pro/channel_patterns.html +++ b/docs/pro/channel_patterns.html @@ -15,8 +15,8 @@ - - + +

    Channel patterns

    Centrifugo PRO enhances a way to configure channels with Channel Patterns feature. This opens a road for building channel model similar to what developers got used to when writing HTTP servers and configuring routes for HTTP request processing.

    diff --git a/docs/pro/channel_state_events.html b/docs/pro/channel_state_events.html index a98067947..b8df2a529 100644 --- a/docs/pro/channel_state_events.html +++ b/docs/pro/channel_state_events.html @@ -15,8 +15,8 @@ - - + +

    Channel state events

    Centrifugo PRO has a feature to enable channel state event webhooks to be sent to your configured backend endpoint:

    diff --git a/docs/pro/client_message_batching.html b/docs/pro/client_message_batching.html index e1c8e5550..c62028bd7 100644 --- a/docs/pro/client_message_batching.html +++ b/docs/pro/client_message_batching.html @@ -15,8 +15,8 @@ - - + +

    Message batching control

    Centrifugo PRO provides advanced options to tweak connection message write behaviour.

    diff --git a/docs/pro/connections.html b/docs/pro/connections.html index ba57c90b0..d8cf02f11 100644 --- a/docs/pro/connections.html +++ b/docs/pro/connections.html @@ -15,8 +15,8 @@ - - + +

    Connections API

    Centrifugo PRO offers an extra API call, connections, which enables retrieval of all active sessions (based on user ID or expression) without the need to activate the presence feature for channels. Furthermore, developers can attach any desired JSON payload to a connection that will then be visible in the result of the connections call. It's worth noting that this additional meta-information remains hidden from the client-side, unlike the info associated with the connection.

    diff --git a/docs/pro/delta_at_most_once.html b/docs/pro/delta_at_most_once.html index a764f13aa..bd38993c1 100644 --- a/docs/pro/delta_at_most_once.html +++ b/docs/pro/delta_at_most_once.html @@ -15,8 +15,8 @@ - - + +

    Delta compression for at most once scenario

    Centrifugo OSS supports delta compression only in channels with recovery and positioning on. To support delta compression for the case when subscribers do not use recovery and positioning Centrifugo PRO provides a boolean namespace option called keep_latest_publication. When it's on – Centrifugo saves latest publication in channels to node's memory and uses it to construct delta updates. The publication lives in node's memory while there are active channel subscribers. This allows dealing with at most once guarantee of Broker's PUB/SUB layer and send deltas properly. So you get efficient at most once broadcast and the reduced bandwidth (of course if delta compression makes sense for data in a channel).

    diff --git a/docs/pro/distributed_rate_limit.html b/docs/pro/distributed_rate_limit.html index 39ed4710b..27e6d889b 100644 --- a/docs/pro/distributed_rate_limit.html +++ b/docs/pro/distributed_rate_limit.html @@ -15,8 +15,8 @@ - - + +

    Distributed rate limit API

    In addition to connection operation rate limiting features Centrifugo PRO provides a generic high precision rate limiting API. It may be used for custom quota managing tasks not even related to real-time connections. Its distributed nature allows managing quotas across different instances of your application backend.

    diff --git a/docs/pro/engine_optimizations.html b/docs/pro/engine_optimizations.html index 914660473..f9980d843 100644 --- a/docs/pro/engine_optimizations.html +++ b/docs/pro/engine_optimizations.html @@ -15,8 +15,8 @@ - - + +

    Engine load optimizations

    Centrifugo PRO comes with several options to reduce load on Engine – specifically on its history and presence API. This may have a positive effect on CPU resource usage on engine side and a positive effect on operation latencies.

    diff --git a/docs/pro/install_and_run.html b/docs/pro/install_and_run.html index ae1a2b3a8..9961890a5 100644 --- a/docs/pro/install_and_run.html +++ b/docs/pro/install_and_run.html @@ -15,8 +15,8 @@ - - + +

    Install and run PRO version

    Centrifugo PRO license agreement

    Centrifugo PRO is distributed by Centrifugal Labs LTD under commercial license which is different from OSS version. By downloading Centrifugo PRO you automatically accept commercial license terms.

    diff --git a/docs/pro/observability_enhancements.html b/docs/pro/observability_enhancements.html index 179ed3b02..e5ee31418 100644 --- a/docs/pro/observability_enhancements.html +++ b/docs/pro/observability_enhancements.html @@ -15,8 +15,8 @@ - - + +

    Observability enhancements

    Centrifugo PRO has some enhancements to exposed metrics. At this moment it provides channel namespace resolution to the following metrics:

    diff --git a/docs/pro/overview.html b/docs/pro/overview.html index 0ca36f52a..ddde92868 100644 --- a/docs/pro/overview.html +++ b/docs/pro/overview.html @@ -15,8 +15,8 @@ - - + +

    Centrifugo PRO ♻️

    Centrifugo PRO is the enhanced version of Centrifugo offered by Centrifugal Labs LTD under a commercial license. It's packed with a unique set of features designed to fit requirements of corporate and enterprise environments, decrease costs at scale, and benefit from additional features such as push notifications support, real-time analytics, and so on. We have leveraged our extensive experience to build Centrifugo PRO, ensuring its extra powers are practical and ready for production workloads.

    diff --git a/docs/pro/performance.html b/docs/pro/performance.html index 9f03f79de..8481bc69b 100644 --- a/docs/pro/performance.html +++ b/docs/pro/performance.html @@ -15,8 +15,8 @@ - - + +

    Faster performance

    diff --git a/docs/pro/process_stats.html b/docs/pro/process_stats.html index 1e96045b6..25b853d45 100644 --- a/docs/pro/process_stats.html +++ b/docs/pro/process_stats.html @@ -15,8 +15,8 @@ - - + +

    CPU and RSS stats

    A useful addition of Centrifugo PRO is an ability to show CPU and RSS memory usage of each node in admin web UI.

    diff --git a/docs/pro/push_notifications.html b/docs/pro/push_notifications.html index 7f14bc404..aa6b4a990 100644 --- a/docs/pro/push_notifications.html +++ b/docs/pro/push_notifications.html @@ -15,8 +15,8 @@ - - + +

    Push notification API

    Centrifugo excels in delivering real-time in-app messages to online users. Sometimes though you need a way to engage offline users to come back to your app. Or trigger some update in the app while it's running in the background. That's where push notifications may be used. Push notifications delivered over battery-efficient platform-dependent transport.

    diff --git a/docs/pro/rate_limiting.html b/docs/pro/rate_limiting.html index ceac780e3..977dc2e0b 100644 --- a/docs/pro/rate_limiting.html +++ b/docs/pro/rate_limiting.html @@ -15,8 +15,8 @@ - - + +

    Operation rate limits

    The rate limit feature allows limiting the number of operations each connection or user can issue during a configured time interval. This is useful to protect the system from misusing, detecting and disconnecting abusive or broken (due to the bug in the frontend application) clients which add unwanted load on a server.

    diff --git a/docs/pro/token_revocation.html b/docs/pro/token_revocation.html index 5b0d4b3fb..c180f4a8f 100644 --- a/docs/pro/token_revocation.html +++ b/docs/pro/token_revocation.html @@ -15,8 +15,8 @@ - - + +

    Token revocation API

    One more protective instrument in Centrifugo PRO is API to manage token revocations.

    diff --git a/docs/pro/tracing.html b/docs/pro/tracing.html index 5bb003547..e9fbcfce5 100644 --- a/docs/pro/tracing.html +++ b/docs/pro/tracing.html @@ -15,8 +15,8 @@ - - + +

    User and channel tracing

    That's a unique thing. The tracing feature of Centrifugo PRO allows attaching to any channel to see all messages flying towards subscribers or attach to a specific user ID to see all user-related events in real-time.

    diff --git a/docs/pro/user_block.html b/docs/pro/user_block.html index 46734232a..d11fee6c8 100644 --- a/docs/pro/user_block.html +++ b/docs/pro/user_block.html @@ -15,8 +15,8 @@ - - + +

    User blocking API

    One additional instrument for making protective actions in Centrifugo PRO is user blocking API which allows blocking a specific user on Centrifugo level.

    diff --git a/docs/pro/user_status.html b/docs/pro/user_status.html index 46989d3d4..1458ee3fd 100644 --- a/docs/pro/user_status.html +++ b/docs/pro/user_status.html @@ -15,8 +15,8 @@ - - + +

    User status API

    Centrifugo OSS provides a presence feature for channels. It works well (for channels with reasonably small number of active subscribers though), but sometimes you may need a bit different functionality.

    diff --git a/docs/server/admin_web.html b/docs/server/admin_web.html index b42e43805..424fbad35 100644 --- a/docs/server/admin_web.html +++ b/docs/server/admin_web.html @@ -15,8 +15,8 @@ - - + +

    Admin web UI

    Centrifugo comes with a built-in administrative web interface. It enables users to:

    diff --git a/docs/server/authentication.html b/docs/server/authentication.html index 23989ab72..7b03b2856 100644 --- a/docs/server/authentication.html +++ b/docs/server/authentication.html @@ -15,8 +15,8 @@ - - + +

    Client JWT authentication

    To securely authenticate incoming real-time client connections, Centrifugo can use a JSON Web Token (JWT) issued by your application backend. This process allows Centrifugo to identify the user's ID in your application securely. Additionally, your application can include extra information within the JWT claims, which Centrifugo can then utilize. This chapter will explain how such connection token may be created and used.

    diff --git a/docs/server/cache_recovery.html b/docs/server/cache_recovery.html index 2a106001a..f7fc53e91 100644 --- a/docs/server/cache_recovery.html +++ b/docs/server/cache_recovery.html @@ -15,8 +15,8 @@ - - + +

    Cache recovery mode

    Cache recovery mode in channels is designed to quickly deliver the most recent (latest) publication as the first event to the subscriber right after subscription request. This functionality allows Centrifugo channels to behave as a real-time key-value store. The feature is available since Centrifugo v5.4.0.

    diff --git a/docs/server/channel_permissions.html b/docs/server/channel_permissions.html index 2d57f505c..6cc04f9b6 100644 --- a/docs/server/channel_permissions.html +++ b/docs/server/channel_permissions.html @@ -15,8 +15,8 @@ - - + +

    Channel permission model

    When using Centrifugo server API you don't need to think about channel permissions at all – everything is allowed. In server API case, request to Centrifugo must be issued by your application backend – so you have all the power to check any required permissions before issuing API request to Centrifugo.

    diff --git a/docs/server/channel_token_auth.html b/docs/server/channel_token_auth.html index 06f4f2b5c..9555a29b5 100644 --- a/docs/server/channel_token_auth.html +++ b/docs/server/channel_token_auth.html @@ -15,8 +15,8 @@ - - + +

    Channel JWT authorization

    In the chapter about channel permissions we mentioned that to subscribe on a channel client can provide subscription token. This chapter has more information about the subscription token mechanism in Centrifugo.

    diff --git a/docs/server/channels.html b/docs/server/channels.html index 541942d17..7e867e20c 100644 --- a/docs/server/channels.html +++ b/docs/server/channels.html @@ -15,8 +15,8 @@ - - + +

    Channels and namespaces

    Centrifugo operates on a PUB/SUB model. Upon connecting to a server, clients can subscribe to channels. A channel is one of the core concepts of Centrifugo. Most of the time when integrating Centrifugo, you will work with channels and determine the optimal channel configuration for your application.

    diff --git a/docs/server/codes.html b/docs/server/codes.html index 95465a7ac..6f345c177 100644 --- a/docs/server/codes.html +++ b/docs/server/codes.html @@ -15,8 +15,8 @@ - - + +

    Client protocol codes

    This chapter describes error, unsubscribe and disconnect codes Centrifugo uses in a client protocol, also error codes which a server API can return in response.

    diff --git a/docs/server/configuration.html b/docs/server/configuration.html index c579cb49c..ca1ca68d2 100644 --- a/docs/server/configuration.html +++ b/docs/server/configuration.html @@ -15,8 +15,8 @@ - - + +

    Configure Centrifugo

    Centrifugo can start without any configuration. In most cases though, you need to configure it to set options for server API, connection JWT authentication or maybe authentication over connect proxy, describe the desired channel behaviour, and so on.

    diff --git a/docs/server/console_commands.html b/docs/server/console_commands.html index d42241439..3551ff290 100644 --- a/docs/server/console_commands.html +++ b/docs/server/console_commands.html @@ -15,8 +15,8 @@ - - + +

    Helper CLI commands

    Here is a list of helpful command-line commands that come with Centrifugo executable.

    diff --git a/docs/server/consumers.html b/docs/server/consumers.html index 97d812a2a..6b273184b 100644 --- a/docs/server/consumers.html +++ b/docs/server/consumers.html @@ -15,8 +15,8 @@ - - + +

    Built-in API command async consumers

    In server API chapter we've shown how to execute various Centrifugo server API commands (publish, broadcast, etc.) over HTTP or GRPC. In many cases you will call those APIs from your application business logic synchronously. But to deal with temporary network and availability issues, and achieve reliable execution of API commands upon changes in your primary application database you may want to use queuing techniques and call Centrifugo API asynchronously.

    diff --git a/docs/server/delta_compression.html b/docs/server/delta_compression.html index 566d2d45d..0e4386203 100644 --- a/docs/server/delta_compression.html +++ b/docs/server/delta_compression.html @@ -15,8 +15,8 @@ - - + +

    Delta compression in channels

    Delta compression feature allows a client to subscribe to a channel in a way so that message payloads contain only the differences between the current message and the previous one sent on the channel. The feature is available since Centrifugo v5.4.0.

    diff --git a/docs/server/engines.html b/docs/server/engines.html index a83a22a6c..6a81b681b 100644 --- a/docs/server/engines.html +++ b/docs/server/engines.html @@ -15,8 +15,8 @@ - - + +

    Engines and scalability

    The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data.

    diff --git a/docs/server/history_and_recovery.html b/docs/server/history_and_recovery.html index 8d2dd0b2a..887913588 100644 --- a/docs/server/history_and_recovery.html +++ b/docs/server/history_and_recovery.html @@ -15,8 +15,8 @@ - - + +

    History and recovery

    Centrifugo engines can maintain publication history for channels with configured history size and TTL.

    diff --git a/docs/server/infra_tuning.html b/docs/server/infra_tuning.html index 0fd12a887..6539f7026 100644 --- a/docs/server/infra_tuning.html +++ b/docs/server/infra_tuning.html @@ -15,8 +15,8 @@ - - + +

    Infrastructure tuning

    As Centrifugo deals with lots of persistent connections your operating system and server infrastructure must be ready for it.

    diff --git a/docs/server/load_balancing.html b/docs/server/load_balancing.html index fa86e6e73..7d5ffaa41 100644 --- a/docs/server/load_balancing.html +++ b/docs/server/load_balancing.html @@ -15,8 +15,8 @@ - - + +

    Load balancing

    This chapter shows how to deal with persistent connection load balancing.

    diff --git a/docs/server/monitoring.html b/docs/server/monitoring.html index 8f2acd9a8..26177d0ea 100644 --- a/docs/server/monitoring.html +++ b/docs/server/monitoring.html @@ -15,8 +15,8 @@ - - + +

    Metrics monitoring

    Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.

    diff --git a/docs/server/observability.html b/docs/server/observability.html index 4107ac6a0..bfe862cb7 100644 --- a/docs/server/observability.html +++ b/docs/server/observability.html @@ -15,8 +15,8 @@ - - + +

    Server observability

    To provide a better server observability Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.

    diff --git a/docs/server/presence.html b/docs/server/presence.html index 9b4420c00..da42d7d58 100644 --- a/docs/server/presence.html +++ b/docs/server/presence.html @@ -15,8 +15,8 @@ - - + +

    Online presence

    The online presence feature in Centrifugo is a powerful tool for monitoring and managing active users within a specific channel. It provides a real-time view of users currently subscribed to that channel. Additionally, Centrifugo can emit join and leave events to all channel subscribers whenever clients subscribe to or unsubscribe from a channel, allowing you to track user activity more effectively.

    diff --git a/docs/server/proxy.html b/docs/server/proxy.html index ff5ecdb61..3b50042d4 100644 --- a/docs/server/proxy.html +++ b/docs/server/proxy.html @@ -15,8 +15,8 @@ - - + +

    Proxy events to the backend

    Centrifugo provides a way to proxy client connection events to your application backend, allowing you to respond in a customized manner. In other words, this is a mechanism of sending (web)hooks from Centrifugo to the backend to control the behavior of real-time connections.

    diff --git a/docs/server/proxy_streams.html b/docs/server/proxy_streams.html index 969c28a2c..82a5aa4e2 100644 --- a/docs/server/proxy_streams.html +++ b/docs/server/proxy_streams.html @@ -15,8 +15,8 @@ - - + +

    Proxy subscription streams

    Experimental

    This is an experimental extension of Centrifugo proxy. We appreciate your feedback to make sure it's useful and solves real-world problems before marking it as stable and commit to the API.

    diff --git a/docs/server/server_api.html b/docs/server/server_api.html index 6a4662514..7f5c2e644 100644 --- a/docs/server/server_api.html +++ b/docs/server/server_api.html @@ -15,8 +15,8 @@ - - + +

    Server API walkthrough

    Server API provides various methods to interact with Centrifugo from your application backend. Specifically, in most cases this is an entry point for publications into channels (see publish method). It also allows getting information about Centrifugo cluster, disconnect users, extract channel online presence information, channel history, and so on.

    diff --git a/docs/server/server_subs.html b/docs/server/server_subs.html index 30e11ddc6..88d6a2479 100644 --- a/docs/server/server_subs.html +++ b/docs/server/server_subs.html @@ -15,8 +15,8 @@ - - + +

    Server-side subscriptions

    Centrifugo clients can initiate a subscription to a channel by calling the subscribe method of client API. We call it client-side subscriptions. In most cases, client-side subscriptions is a flexible and recommended approach to subscribe to channels. A frontend usually knows which channels it needs to consume at a concrete moment.

    diff --git a/docs/server/tls.html b/docs/server/tls.html index f8b1eff42..b9970bd22 100644 --- a/docs/server/tls.html +++ b/docs/server/tls.html @@ -15,8 +15,8 @@ - - + +

    Configure TLS

    TLS/SSL layer is very important not only for securing your connections but also to increase a diff --git a/docs/transports/client_api.html b/docs/transports/client_api.html index ec501e263..a78e24e78 100644 --- a/docs/transports/client_api.html +++ b/docs/transports/client_api.html @@ -15,8 +15,8 @@ - - + +

    Client SDK API

    Centrifugo has several client SDKs to establish a real-time connection with a server. Centrifugo SDKs use WebSocket as the main data transport and send/receive messages encoded according to our bidirectional protocol. That protocol is built on top of the Protobuf schema (both JSON and binary Protobuf formats are supported). It provides asynchronous communication, sending RPC, multiplexing subscriptions to channels, etc. Client SDK wraps the protocol and exposes a set of APIs to developers.

    diff --git a/docs/transports/client_protocol.html b/docs/transports/client_protocol.html index 51020a2e2..971b392e7 100644 --- a/docs/transports/client_protocol.html +++ b/docs/transports/client_protocol.html @@ -15,8 +15,8 @@ - - + +

    Client protocol

    This chapter describes the core concepts of Centrifugo bidirectional client protocol – concentrating on framing level. If you want to find out details about exposed client API then look at client API document.

    diff --git a/docs/transports/client_sdk.html b/docs/transports/client_sdk.html index cf6000880..aa2b3e8ee 100644 --- a/docs/transports/client_sdk.html +++ b/docs/transports/client_sdk.html @@ -15,8 +15,8 @@ - - + +

    Client real-time SDKs

    In the previous chapter we investigated common principles of Centrifugo client SDK API. Here we will provide a list of available bidirectional connectors you can use to communicate with Centrifugo.

    diff --git a/docs/transports/http_stream.html b/docs/transports/http_stream.html index a28efd445..165eb9572 100644 --- a/docs/transports/http_stream.html +++ b/docs/transports/http_stream.html @@ -15,8 +15,8 @@ - - + +

    HTTP streaming, with bidirectional emulation

    HTTP streaming is a technique based on using a long-lived HTTP connection between a client and a server with a chunked transfer encoding. Usually it only allows unidirectional flow of messages from server to client but with Centrifugo bidirectional emulation layer it may be used as a full-featured fallback or alternative to WebSocket.

    diff --git a/docs/transports/overview.html b/docs/transports/overview.html index ea4b55f46..7287a4667 100644 --- a/docs/transports/overview.html +++ b/docs/transports/overview.html @@ -15,8 +15,8 @@ - - + +

    Real-time transports

    Centrifugo supports a variety of transports to deliver real-time messages to clients.

    diff --git a/docs/transports/sockjs.html b/docs/transports/sockjs.html index e04f4f94a..c195c2657 100644 --- a/docs/transports/sockjs.html +++ b/docs/transports/sockjs.html @@ -15,8 +15,8 @@ - - + +

    SockJS

    SockJS is a polyfill browser library which provides HTTP-based fallback transports in case when it's not possible to establish Websocket connection. This can happen in old client browsers or because of some proxy behind client and server that cuts of Websocket traffic. You can find more information on SockJS project Github page.

    diff --git a/docs/transports/sse.html b/docs/transports/sse.html index 25d793b45..b5274c547 100644 --- a/docs/transports/sse.html +++ b/docs/transports/sse.html @@ -15,8 +15,8 @@ - - + +

    SSE (EventSource), with bidirectional emulation

    Server-Sent Events or EventSource is a well-known HTTP-based transport available in all modern browsers and loved by many developers. It's unidirectional in its nature but with Centrifugo bidirectional emulation layer it may be used as a fallback or alternative to WebSocket.

    diff --git a/docs/transports/uni_client_protocol.html b/docs/transports/uni_client_protocol.html index 62eec13c6..7a0d3692c 100644 --- a/docs/transports/uni_client_protocol.html +++ b/docs/transports/uni_client_protocol.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional client protocol

    As we mentioned in overview you can avoid using Centrifugo SDKs if you stick with unidirectional approach. In this case though you will need to implement some basic parsing on client side to consume message types sent by Centrifugo into unidirectional connections.

    diff --git a/docs/transports/uni_grpc.html b/docs/transports/uni_grpc.html index b508d4802..2e4a2a1c6 100644 --- a/docs/transports/uni_grpc.html +++ b/docs/transports/uni_grpc.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional GRPC

    It's possible to connect to GRPC unidirectional stream to consume real-time messages from Centrifugo. In this case you need to generate GRPC code for your language on client-side.

    diff --git a/docs/transports/uni_http_stream.html b/docs/transports/uni_http_stream.html index ee208c1c0..1dd4852e5 100644 --- a/docs/transports/uni_http_stream.html +++ b/docs/transports/uni_http_stream.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional HTTP streaming

    HTTP streaming is a technique based on using a long-lived HTTP connection between a client and a server with a chunked transfer encoding. These days it's possible to use it from the web browser using modern Fetch and Readable Streams API.

    diff --git a/docs/transports/uni_sse.html b/docs/transports/uni_sse.html index a665c9374..8a891e9b5 100644 --- a/docs/transports/uni_sse.html +++ b/docs/transports/uni_sse.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional SSE (EventSource)

    Server-Sent Events or EventSource is a well-known HTTP-based transport available in all modern browsers and loved by many developers.

    diff --git a/docs/transports/uni_websocket.html b/docs/transports/uni_websocket.html index 55f6d28e6..28970d5e1 100644 --- a/docs/transports/uni_websocket.html +++ b/docs/transports/uni_websocket.html @@ -15,8 +15,8 @@ - - + +

    Unidirectional WebSocket

    Default unidirectional WebSocket connection endpoint in Centrifugo is:

    diff --git a/docs/transports/websocket.html b/docs/transports/websocket.html index 1a346ab79..72094b64a 100644 --- a/docs/transports/websocket.html +++ b/docs/transports/websocket.html @@ -15,8 +15,8 @@ - - + +

    WebSocket

    Websocket is the main transport in Centrifugo. It's a very efficient low-overhead protocol on top of TCP.

    diff --git a/docs/transports/webtransport.html b/docs/transports/webtransport.html index 401bf4d5f..2d5096a9e 100644 --- a/docs/transports/webtransport.html +++ b/docs/transports/webtransport.html @@ -15,8 +15,8 @@ - - + +

    WebTransport

    WebTransport is an API offering low-latency, bidirectional, client-server messaging on top of HTTP/3 (with QUIC under the hood). See Using WebTransport article that gives a good overview of it.

    diff --git a/docs/tutorial/backend.html b/docs/tutorial/backend.html index fa000871b..366917cf7 100644 --- a/docs/tutorial/backend.html +++ b/docs/tutorial/backend.html @@ -15,8 +15,8 @@ - - + +

    Setting up backend and database

    Let's start building the app. As the first step, create a directory for the new app:

    diff --git a/docs/tutorial/centrifugo.html b/docs/tutorial/centrifugo.html index b600ca578..b8c3a2c53 100644 --- a/docs/tutorial/centrifugo.html +++ b/docs/tutorial/centrifugo.html @@ -15,8 +15,8 @@ - - + +

    Integrating Centrifugo for real-time event delivery

    It's finally time for the real-time! In some cases you already have an application and when integrating Centrifugo you start from here.

    diff --git a/docs/tutorial/frontend.html b/docs/tutorial/frontend.html index 198a2b177..279882582 100644 --- a/docs/tutorial/frontend.html +++ b/docs/tutorial/frontend.html @@ -15,8 +15,8 @@ - - + +

    Creating SPA frontend with React

    On the frontend we will use Vite with React and Typescript. In this tutorial we are not paying a lot of attention to making all the types strict and using any a lot. Which is actually a point for improvement, but at least helps to make the tutorial slightly shorter. The prerequisites is NodeJS >= 18.

    diff --git a/docs/tutorial/improvements.html b/docs/tutorial/improvements.html index 973f151b9..7aaec7744 100644 --- a/docs/tutorial/improvements.html +++ b/docs/tutorial/improvements.html @@ -15,8 +15,8 @@ - - + +

    Appendix #1: Possible Improvements

    There are still many areas for improvement in GrandChat, but we had to halt at a certain point to prevent the tutorial from becoming a book. If you enjoyed the tutorial and wish to enhance GrandChat further, here are some bright ideas:

    diff --git a/docs/tutorial/intro.html b/docs/tutorial/intro.html index 5ad3d3b2d..0e31afaf0 100644 --- a/docs/tutorial/intro.html +++ b/docs/tutorial/intro.html @@ -15,8 +15,8 @@ - - + +

    Building WebSocket chat (messenger) app from scratch

    In this tutorial, we show how to build a rather complex real-time application with Centrifugo. It features a modern and responsive frontend, user authentication, channel permission checks, and the main database as a source of truth.

    diff --git a/docs/tutorial/layout.html b/docs/tutorial/layout.html index f608ded0d..486c52f34 100644 --- a/docs/tutorial/layout.html +++ b/docs/tutorial/layout.html @@ -15,8 +15,8 @@ - - + +

    App layout and behavior

    Before we start, we would like the reader to be more familiar with the layout and behavior of the application we are creating here. Let's look at it screen by screen, describe the behavior, and explain which parts will be endowed with real-time superpowers.

    diff --git a/docs/tutorial/outbox_cdc.html b/docs/tutorial/outbox_cdc.html index 41fdd2bc9..44c6a115d 100644 --- a/docs/tutorial/outbox_cdc.html +++ b/docs/tutorial/outbox_cdc.html @@ -15,8 +15,8 @@ - - + +

    Broadcast using transactional outbox and CDC

    Some of you may notice one potential issue which could prevent event delivery to users when publishing messages to Centrifugo API. Since we do this after a transaction and via a network call (in our case, using HTTP), it means the broadcast API call may return an error.

    diff --git a/docs/tutorial/outro.html b/docs/tutorial/outro.html index 5c5f388d5..21ed09dc8 100644 --- a/docs/tutorial/outro.html +++ b/docs/tutorial/outro.html @@ -15,8 +15,8 @@ - - + +

    Wrapping up – things learnt

    At this point, we have a working real-time app, so the tutorial comes to an end. We've covered some concepts of Centrifugo, such as:

    diff --git a/docs/tutorial/recovery.html b/docs/tutorial/recovery.html index 34da6c339..e531a8b73 100644 --- a/docs/tutorial/recovery.html +++ b/docs/tutorial/recovery.html @@ -15,8 +15,8 @@ - - + +

    Missed messages recovery

    At this point, we already have a real-time application with the instant delivery of events to interested messenger users. Now, let's focus on ensuring reliable message delivery. The first step would be enabling Centrifugo's automatic message recovery for personal channels.

    diff --git a/docs/tutorial/reverse_proxy.html b/docs/tutorial/reverse_proxy.html index cca465750..a416434a8 100644 --- a/docs/tutorial/reverse_proxy.html +++ b/docs/tutorial/reverse_proxy.html @@ -15,8 +15,8 @@ - - + +

    Adding Nginx as a reverse proxy

    As mentioned, we are building a single-page frontend application here, and the frontend will be completely decoupled from the backend. This separation is advantageous because Centrifugo users can theoretically swap only the backend or frontend components while following this tutorial. For example, one could keep the frontend part but attempt to implement the backend in Laravel, Rails, or another framework.

    diff --git a/docs/tutorial/scale.html b/docs/tutorial/scale.html index 6273bbfb6..4b55b86b6 100644 --- a/docs/tutorial/scale.html +++ b/docs/tutorial/scale.html @@ -15,8 +15,8 @@ - - + +

    Scale to 100k cats in room

    Congratulations – we've built an awesome app and we are done with the development within this tutorial! 🎉

    diff --git a/docs/tutorial/tips_and_tricks.html b/docs/tutorial/tips_and_tricks.html index fbae7beab..75a444f93 100644 --- a/docs/tutorial/tips_and_tricks.html +++ b/docs/tutorial/tips_and_tricks.html @@ -15,8 +15,8 @@ - - + +

    Appendix #2: Tips and tricks

    Making this tutorial took quite a lot of time for us. We want to collect some useful tips and tricks here for those who decide to play with the final example. Feel free to contribute if you find something which could help others.

    diff --git a/img/.DS_Store b/img/.DS_Store index c2632c56836f0775ef0ade66d148718ad97371ee..8af5868fd8578ee6af2a6e381d7b902c398dd1cc 100644 GIT binary patch delta 34 qcmZo^U~Fk%oZ!#czcFApKcmTJ9)Yzyn?K4(F;8r;*vzf)Qyu`*KU(2|<0pvAIziHtt;W^RR_@&Hx~ B4wL`@ diff --git a/index.html b/index.html index 2ba2e459c..21fa77b5c 100644 --- a/index.html +++ b/index.html @@ -15,8 +15,8 @@ - - + +
    CENTRIFUGO
    Scalable real-time messaging server. Set up once and forever.
    USED IN PRODUCTS OF SUCCESSFUL COMPANIES
    THOUSANDS OF REAL INSTALLATIONS
    Seamless Integration

    Seamless Integration

    Centrifugo is a self-hosted service which handles connections over various transports and provides a simple publishing API. Centrifugo nicely integrates with any application — no changes in the existing app architecture required to introduce real-time updates.

    Great Performance

    Great Performance

    Centrifugo is written in Go language and includes some smart optimizations. See the description of the test stand with one million WebSocket connections and 30 million delivered messages per minute on hardware comparable to a single modern server.

    Feature-rich

    Feature-rich

    Centrifugo provides flexible auth, various types of subscriptions, channel history, online presence, delta updates, the ability to proxy connection events to the backend, and much more. It comes with official SDK libraries for both web and mobile development.

    Out-of-the-box Scalability

    Out-of-the-box Scalability

    Scale connections over many Centrifugo nodes by using built-in integrations with efficient brokers: Redis (or Redis Cluster, or Redis-compatible storages like AWS Elasticache, DragonflyDB, Valkey, KeyDB, with client-side sharding support), Tarantool and Nats.

    Proven in Production

    Proven in Production

    Started a decade ago, Centrifugo (and Centrifuge library for Go it's built on top of) is mature, battle-tested software that has been successfully used in production by many companies around the world: VK, Badoo, ManyChat, OpenWeb, Grafana, and others.

    Centrifugo PRO

    Centrifugo PRO

    Centrifugo PRO offers great benefits for corporate and enterprise environments by providing unique features on top of the OSS version: analytics with ClickHouse, real-time tracing, performance optimizations, push notification API, SSO integrations for web UI, etc.

    What is real-time messaging?

    Real-time messaging is used to create interactive applications where events are delivered to online users with minimal delay.

    Chats apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, etc. can all be built on top of a real-time messaging system.

    Centrifugo is a user facing PUB/SUB server that handles persistent connections over various real-time transports — WebSocket, HTTP-streaming, SSE (Server-Sent Events), WebTransport, GRPC.

    Looking for a cool demo?

    Here is the real-time telemetry streamed from the Assetto Corsa racing simulator to the Grafana dashboard with a help of our WebSocket technologies.

    This demonstrates that you can stream 60Hz data towards client connections and thus provide instant visual feedback on the state of the system.

    Slack-scale messenger?

    Straightforward with Centrifugo! Even though your backend does not support concurrency. See the tutorial where we build a beautiful messenger app and go beyond usually shown basics.

    Centrifugo is a versatile real-time component – it can be used to build various types of real-time applications, not just messengers.

    Rotating Image

    Are you Enterprise?

    Centrifugal Labs offers a PRO version of Centrifugo that includes a set of unique features, additional APIs, and enhanced performance. Ever dreamed about a self-hosted real-time messaging system combined with a push notification system? Want to benefit from analytics of real-time connections and subscriptions? Centrifugo PRO makes this possible. And much more actually.

    diff --git a/license.html b/license.html index a1e66ff33..c86b6cafd 100644 --- a/license.html +++ b/license.html @@ -15,8 +15,8 @@ - - + +

    Centrifugal Labs LTD license agreement

    diff --git a/license_exchange_lemon.html b/license_exchange_lemon.html index cc3563f4a..21f5465b7 100644 --- a/license_exchange_lemon.html +++ b/license_exchange_lemon.html @@ -15,8 +15,8 @@ - - + +

    Thanks for purchasing Centrifugo PRO 🎉

    diff --git a/search.html b/search.html index ff344da7b..0359a5489 100644 --- a/search.html +++ b/search.html @@ -15,8 +15,8 @@ - - + + diff --git a/sitemap.xml b/sitemap.xml index 5658eaed7..91fbbd006 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://centrifugal.dev/blogweekly0.5https://centrifugal.dev/blog/2020/02/10/million-connections-with-centrifugoweekly0.5https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transportweekly0.5https://centrifugal.dev/blog/2020/11/12/scaling-websocketweekly0.5https://centrifugal.dev/blog/2021/01/15/centrifuge-introweekly0.5https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3weekly0.5https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejsweekly0.5https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-applicationweekly0.5https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorialweekly0.5https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-releasedweekly0.5https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribeweekly0.5https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performanceweekly0.5https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugoweekly0.5https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-releasedweekly0.5https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthosweekly0.5https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitxweekly0.5https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notificationsweekly0.5https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptionsweekly0.5https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experimentsweekly0.5https://centrifugal.dev/blog/archiveweekly0.5https://centrifugal.dev/blog/tagsweekly0.5https://centrifugal.dev/blog/tags/authenticationweekly0.5https://centrifugal.dev/blog/tags/benthosweekly0.5https://centrifugal.dev/blog/tags/centrifugeweekly0.5https://centrifugal.dev/blog/tags/centrifugoweekly0.5https://centrifugal.dev/blog/tags/compressionweekly0.5https://centrifugal.dev/blog/tags/djangoweekly0.5https://centrifugal.dev/blog/tags/goweekly0.5https://centrifugal.dev/blog/tags/grpcweekly0.5https://centrifugal.dev/blog/tags/interviewweekly0.5https://centrifugal.dev/blog/tags/keycloakweekly0.5https://centrifugal.dev/blog/tags/laravelweekly0.5https://centrifugal.dev/blog/tags/lokiweekly0.5https://centrifugal.dev/blog/tags/phpweekly0.5https://centrifugal.dev/blog/tags/proweekly0.5https://centrifugal.dev/blog/tags/proxyweekly0.5https://centrifugal.dev/blog/tags/push-notificationsweekly0.5https://centrifugal.dev/blog/tags/quicweekly0.5https://centrifugal.dev/blog/tags/redisweekly0.5https://centrifugal.dev/blog/tags/releaseweekly0.5https://centrifugal.dev/blog/tags/ssoweekly0.5https://centrifugal.dev/blog/tags/tutorialweekly0.5https://centrifugal.dev/blog/tags/usecaseweekly0.5https://centrifugal.dev/blog/tags/websocketweekly0.5https://centrifugal.dev/blog/tags/webtransportweekly0.5https://centrifugal.dev/components/Highlightweekly0.5https://centrifugal.dev/components/logoweekly0.5https://centrifugal.dev/components/logos/Badooweekly0.5https://centrifugal.dev/components/logos/Grafanaweekly0.5https://centrifugal.dev/components/logos/ManyChatweekly0.5https://centrifugal.dev/components/logos/OpenWebweekly0.5https://centrifugal.dev/licenseweekly0.5https://centrifugal.dev/searchweekly0.5https://centrifugal.dev/docs/3/attributionsweekly0.5https://centrifugal.dev/docs/3/ecosystem/centrifugeweekly0.5https://centrifugal.dev/docs/3/ecosystem/integrationsweekly0.5https://centrifugal.dev/docs/3/faqweekly0.5https://centrifugal.dev/docs/3/flow_diagramsweekly0.5https://centrifugal.dev/docs/3/getting-started/client_apiweekly0.5https://centrifugal.dev/docs/3/getting-started/designweekly0.5https://centrifugal.dev/docs/3/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/3/getting-started/installationweekly0.5https://centrifugal.dev/docs/3/getting-started/integrationweekly0.5https://centrifugal.dev/docs/3/getting-started/introductionweekly0.5https://centrifugal.dev/docs/3/getting-started/migration_v3weekly0.5https://centrifugal.dev/docs/3/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/3/pro/analyticsweekly0.5https://centrifugal.dev/docs/3/pro/db_namespacesweekly0.5https://centrifugal.dev/docs/3/pro/install_and_runweekly0.5https://centrifugal.dev/docs/3/pro/overviewweekly0.5https://centrifugal.dev/docs/3/pro/performanceweekly0.5https://centrifugal.dev/docs/3/pro/process_statsweekly0.5https://centrifugal.dev/docs/3/pro/singleflightweekly0.5https://centrifugal.dev/docs/3/pro/throttlingweekly0.5https://centrifugal.dev/docs/3/pro/token_revocationweekly0.5https://centrifugal.dev/docs/3/pro/tracingweekly0.5https://centrifugal.dev/docs/3/pro/user_blockweekly0.5https://centrifugal.dev/docs/3/pro/user_connectionsweekly0.5https://centrifugal.dev/docs/3/pro/user_statusweekly0.5https://centrifugal.dev/docs/3/server/admin_webweekly0.5https://centrifugal.dev/docs/3/server/authenticationweekly0.5https://centrifugal.dev/docs/3/server/channelsweekly0.5https://centrifugal.dev/docs/3/server/codesweekly0.5https://centrifugal.dev/docs/3/server/configurationweekly0.5https://centrifugal.dev/docs/3/server/console_commandsweekly0.5https://centrifugal.dev/docs/3/server/enginesweekly0.5https://centrifugal.dev/docs/3/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/3/server/infra_tuningweekly0.5https://centrifugal.dev/docs/3/server/load_balancingweekly0.5https://centrifugal.dev/docs/3/server/monitoringweekly0.5https://centrifugal.dev/docs/3/server/private_channelsweekly0.5https://centrifugal.dev/docs/3/server/proxyweekly0.5https://centrifugal.dev/docs/3/server/server_apiweekly0.5https://centrifugal.dev/docs/3/server/server_subsweekly0.5https://centrifugal.dev/docs/3/server/tlsweekly0.5https://centrifugal.dev/docs/3/transports/client_protocolweekly0.5https://centrifugal.dev/docs/3/transports/client_sdkweekly0.5https://centrifugal.dev/docs/3/transports/overviewweekly0.5https://centrifugal.dev/docs/3/transports/sockjsweekly0.5https://centrifugal.dev/docs/3/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/3/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/3/transports/uni_sseweekly0.5https://centrifugal.dev/docs/3/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/3/transports/websocketweekly0.5https://centrifugal.dev/docs/4/attributionsweekly0.5https://centrifugal.dev/docs/4/faqweekly0.5https://centrifugal.dev/docs/4/flow_diagramsweekly0.5https://centrifugal.dev/docs/4/getting-started/client_apiweekly0.5https://centrifugal.dev/docs/4/getting-started/communityweekly0.5https://centrifugal.dev/docs/4/getting-started/designweekly0.5https://centrifugal.dev/docs/4/getting-started/ecosystemweekly0.5https://centrifugal.dev/docs/4/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/4/getting-started/installationweekly0.5https://centrifugal.dev/docs/4/getting-started/integrationweekly0.5https://centrifugal.dev/docs/4/getting-started/introductionweekly0.5https://centrifugal.dev/docs/4/getting-started/migration_v4weekly0.5https://centrifugal.dev/docs/4/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/4/pro/analyticsweekly0.5https://centrifugal.dev/docs/4/pro/capabilitiesweekly0.5https://centrifugal.dev/docs/4/pro/cel_expressionsweekly0.5https://centrifugal.dev/docs/4/pro/channel_patternsweekly0.5https://centrifugal.dev/docs/4/pro/client_message_batchingweekly0.5https://centrifugal.dev/docs/4/pro/connectionsweekly0.5https://centrifugal.dev/docs/4/pro/install_and_runweekly0.5https://centrifugal.dev/docs/4/pro/overviewweekly0.5https://centrifugal.dev/docs/4/pro/performanceweekly0.5https://centrifugal.dev/docs/4/pro/process_statsweekly0.5https://centrifugal.dev/docs/4/pro/push_notificationsweekly0.5https://centrifugal.dev/docs/4/pro/singleflightweekly0.5https://centrifugal.dev/docs/4/pro/throttlingweekly0.5https://centrifugal.dev/docs/4/pro/token_revocationweekly0.5https://centrifugal.dev/docs/4/pro/tracingweekly0.5https://centrifugal.dev/docs/4/pro/user_blockweekly0.5https://centrifugal.dev/docs/4/pro/user_statusweekly0.5https://centrifugal.dev/docs/4/server/admin_webweekly0.5https://centrifugal.dev/docs/4/server/authenticationweekly0.5https://centrifugal.dev/docs/4/server/channel_permissionsweekly0.5https://centrifugal.dev/docs/4/server/channel_token_authweekly0.5https://centrifugal.dev/docs/4/server/channelsweekly0.5https://centrifugal.dev/docs/4/server/codesweekly0.5https://centrifugal.dev/docs/4/server/configurationweekly0.5https://centrifugal.dev/docs/4/server/console_commandsweekly0.5https://centrifugal.dev/docs/4/server/enginesweekly0.5https://centrifugal.dev/docs/4/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/4/server/infra_tuningweekly0.5https://centrifugal.dev/docs/4/server/load_balancingweekly0.5https://centrifugal.dev/docs/4/server/monitoringweekly0.5https://centrifugal.dev/docs/4/server/presenceweekly0.5https://centrifugal.dev/docs/4/server/proxyweekly0.5https://centrifugal.dev/docs/4/server/server_apiweekly0.5https://centrifugal.dev/docs/4/server/server_subsweekly0.5https://centrifugal.dev/docs/4/server/tlsweekly0.5https://centrifugal.dev/docs/4/transports/client_apiweekly0.5https://centrifugal.dev/docs/4/transports/client_protocolweekly0.5https://centrifugal.dev/docs/4/transports/client_sdkweekly0.5https://centrifugal.dev/docs/4/transports/http_streamweekly0.5https://centrifugal.dev/docs/4/transports/overviewweekly0.5https://centrifugal.dev/docs/4/transports/sockjsweekly0.5https://centrifugal.dev/docs/4/transports/sseweekly0.5https://centrifugal.dev/docs/4/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/4/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/4/transports/uni_sseweekly0.5https://centrifugal.dev/docs/4/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/4/transports/websocketweekly0.5https://centrifugal.dev/docs/4/transports/webtransportweekly0.5https://centrifugal.dev/docs/attributionsweekly0.5https://centrifugal.dev/docs/faqweekly0.5https://centrifugal.dev/docs/flow_diagramsweekly0.5https://centrifugal.dev/docs/getting-started/communityweekly0.5https://centrifugal.dev/docs/getting-started/comparisonsweekly0.5https://centrifugal.dev/docs/getting-started/designweekly0.5https://centrifugal.dev/docs/getting-started/ecosystemweekly0.5https://centrifugal.dev/docs/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/getting-started/installationweekly0.5https://centrifugal.dev/docs/getting-started/integrationweekly0.5https://centrifugal.dev/docs/getting-started/introductionweekly0.5https://centrifugal.dev/docs/getting-started/migration_v4weekly0.5https://centrifugal.dev/docs/getting-started/migration_v5weekly0.5https://centrifugal.dev/docs/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/pro/admin_idp_authweekly0.5https://centrifugal.dev/docs/pro/analyticsweekly0.5https://centrifugal.dev/docs/pro/capabilitiesweekly0.5https://centrifugal.dev/docs/pro/cel_expressionsweekly0.5https://centrifugal.dev/docs/pro/channel_cache_emptyweekly0.5https://centrifugal.dev/docs/pro/channel_patternsweekly0.5https://centrifugal.dev/docs/pro/channel_state_eventsweekly0.5https://centrifugal.dev/docs/pro/client_message_batchingweekly0.5https://centrifugal.dev/docs/pro/connectionsweekly0.5https://centrifugal.dev/docs/pro/delta_at_most_onceweekly0.5https://centrifugal.dev/docs/pro/distributed_rate_limitweekly0.5https://centrifugal.dev/docs/pro/engine_optimizationsweekly0.5https://centrifugal.dev/docs/pro/install_and_runweekly0.5https://centrifugal.dev/docs/pro/observability_enhancementsweekly0.5https://centrifugal.dev/docs/pro/overviewweekly0.5https://centrifugal.dev/docs/pro/performanceweekly0.5https://centrifugal.dev/docs/pro/process_statsweekly0.5https://centrifugal.dev/docs/pro/push_notificationsweekly0.5https://centrifugal.dev/docs/pro/rate_limitingweekly0.5https://centrifugal.dev/docs/pro/token_revocationweekly0.5https://centrifugal.dev/docs/pro/tracingweekly0.5https://centrifugal.dev/docs/pro/user_blockweekly0.5https://centrifugal.dev/docs/pro/user_statusweekly0.5https://centrifugal.dev/docs/server/admin_webweekly0.5https://centrifugal.dev/docs/server/authenticationweekly0.5https://centrifugal.dev/docs/server/cache_recoveryweekly0.5https://centrifugal.dev/docs/server/channel_permissionsweekly0.5https://centrifugal.dev/docs/server/channel_token_authweekly0.5https://centrifugal.dev/docs/server/channelsweekly0.5https://centrifugal.dev/docs/server/codesweekly0.5https://centrifugal.dev/docs/server/configurationweekly0.5https://centrifugal.dev/docs/server/console_commandsweekly0.5https://centrifugal.dev/docs/server/consumersweekly0.5https://centrifugal.dev/docs/server/delta_compressionweekly0.5https://centrifugal.dev/docs/server/enginesweekly0.5https://centrifugal.dev/docs/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/server/infra_tuningweekly0.5https://centrifugal.dev/docs/server/load_balancingweekly0.5https://centrifugal.dev/docs/server/monitoringweekly0.5https://centrifugal.dev/docs/server/observabilityweekly0.5https://centrifugal.dev/docs/server/presenceweekly0.5https://centrifugal.dev/docs/server/proxyweekly0.5https://centrifugal.dev/docs/server/proxy_streamsweekly0.5https://centrifugal.dev/docs/server/server_apiweekly0.5https://centrifugal.dev/docs/server/server_subsweekly0.5https://centrifugal.dev/docs/server/tlsweekly0.5https://centrifugal.dev/docs/transports/client_apiweekly0.5https://centrifugal.dev/docs/transports/client_protocolweekly0.5https://centrifugal.dev/docs/transports/client_sdkweekly0.5https://centrifugal.dev/docs/transports/http_streamweekly0.5https://centrifugal.dev/docs/transports/overviewweekly0.5https://centrifugal.dev/docs/transports/sockjsweekly0.5https://centrifugal.dev/docs/transports/sseweekly0.5https://centrifugal.dev/docs/transports/uni_client_protocolweekly0.5https://centrifugal.dev/docs/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/transports/uni_sseweekly0.5https://centrifugal.dev/docs/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/transports/websocketweekly0.5https://centrifugal.dev/docs/transports/webtransportweekly0.5https://centrifugal.dev/docs/tutorial/backendweekly0.5https://centrifugal.dev/docs/tutorial/centrifugoweekly0.5https://centrifugal.dev/docs/tutorial/frontendweekly0.5https://centrifugal.dev/docs/tutorial/improvementsweekly0.5https://centrifugal.dev/docs/tutorial/introweekly0.5https://centrifugal.dev/docs/tutorial/layoutweekly0.5https://centrifugal.dev/docs/tutorial/outbox_cdcweekly0.5https://centrifugal.dev/docs/tutorial/outroweekly0.5https://centrifugal.dev/docs/tutorial/recoveryweekly0.5https://centrifugal.dev/docs/tutorial/reverse_proxyweekly0.5https://centrifugal.dev/docs/tutorial/scaleweekly0.5https://centrifugal.dev/docs/tutorial/tips_and_tricksweekly0.5https://centrifugal.dev/weekly0.5 \ No newline at end of file +https://centrifugal.dev/blogweekly0.5https://centrifugal.dev/blog/2020/02/10/million-connections-with-centrifugoweekly0.5https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transportweekly0.5https://centrifugal.dev/blog/2020/11/12/scaling-websocketweekly0.5https://centrifugal.dev/blog/2021/01/15/centrifuge-introweekly0.5https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3weekly0.5https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejsweekly0.5https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-applicationweekly0.5https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorialweekly0.5https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-releasedweekly0.5https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribeweekly0.5https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performanceweekly0.5https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugoweekly0.5https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-releasedweekly0.5https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthosweekly0.5https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitxweekly0.5https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notificationsweekly0.5https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptionsweekly0.5https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experimentsweekly0.5https://centrifugal.dev/blog/2024/06/03/real-time-document-state-syncweekly0.5https://centrifugal.dev/blog/archiveweekly0.5https://centrifugal.dev/blog/tagsweekly0.5https://centrifugal.dev/blog/tags/authenticationweekly0.5https://centrifugal.dev/blog/tags/benthosweekly0.5https://centrifugal.dev/blog/tags/centrifugeweekly0.5https://centrifugal.dev/blog/tags/centrifugoweekly0.5https://centrifugal.dev/blog/tags/compressionweekly0.5https://centrifugal.dev/blog/tags/djangoweekly0.5https://centrifugal.dev/blog/tags/docsyncweekly0.5https://centrifugal.dev/blog/tags/goweekly0.5https://centrifugal.dev/blog/tags/grpcweekly0.5https://centrifugal.dev/blog/tags/interviewweekly0.5https://centrifugal.dev/blog/tags/keycloakweekly0.5https://centrifugal.dev/blog/tags/laravelweekly0.5https://centrifugal.dev/blog/tags/lokiweekly0.5https://centrifugal.dev/blog/tags/phpweekly0.5https://centrifugal.dev/blog/tags/proweekly0.5https://centrifugal.dev/blog/tags/proxyweekly0.5https://centrifugal.dev/blog/tags/push-notificationsweekly0.5https://centrifugal.dev/blog/tags/quicweekly0.5https://centrifugal.dev/blog/tags/redisweekly0.5https://centrifugal.dev/blog/tags/releaseweekly0.5https://centrifugal.dev/blog/tags/ssoweekly0.5https://centrifugal.dev/blog/tags/tutorialweekly0.5https://centrifugal.dev/blog/tags/usecaseweekly0.5https://centrifugal.dev/blog/tags/websocketweekly0.5https://centrifugal.dev/blog/tags/webtransportweekly0.5https://centrifugal.dev/components/Highlightweekly0.5https://centrifugal.dev/components/logoweekly0.5https://centrifugal.dev/components/logos/Badooweekly0.5https://centrifugal.dev/components/logos/Grafanaweekly0.5https://centrifugal.dev/components/logos/ManyChatweekly0.5https://centrifugal.dev/components/logos/OpenWebweekly0.5https://centrifugal.dev/licenseweekly0.5https://centrifugal.dev/searchweekly0.5https://centrifugal.dev/docs/3/attributionsweekly0.5https://centrifugal.dev/docs/3/ecosystem/centrifugeweekly0.5https://centrifugal.dev/docs/3/ecosystem/integrationsweekly0.5https://centrifugal.dev/docs/3/faqweekly0.5https://centrifugal.dev/docs/3/flow_diagramsweekly0.5https://centrifugal.dev/docs/3/getting-started/client_apiweekly0.5https://centrifugal.dev/docs/3/getting-started/designweekly0.5https://centrifugal.dev/docs/3/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/3/getting-started/installationweekly0.5https://centrifugal.dev/docs/3/getting-started/integrationweekly0.5https://centrifugal.dev/docs/3/getting-started/introductionweekly0.5https://centrifugal.dev/docs/3/getting-started/migration_v3weekly0.5https://centrifugal.dev/docs/3/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/3/pro/analyticsweekly0.5https://centrifugal.dev/docs/3/pro/db_namespacesweekly0.5https://centrifugal.dev/docs/3/pro/install_and_runweekly0.5https://centrifugal.dev/docs/3/pro/overviewweekly0.5https://centrifugal.dev/docs/3/pro/performanceweekly0.5https://centrifugal.dev/docs/3/pro/process_statsweekly0.5https://centrifugal.dev/docs/3/pro/singleflightweekly0.5https://centrifugal.dev/docs/3/pro/throttlingweekly0.5https://centrifugal.dev/docs/3/pro/token_revocationweekly0.5https://centrifugal.dev/docs/3/pro/tracingweekly0.5https://centrifugal.dev/docs/3/pro/user_blockweekly0.5https://centrifugal.dev/docs/3/pro/user_connectionsweekly0.5https://centrifugal.dev/docs/3/pro/user_statusweekly0.5https://centrifugal.dev/docs/3/server/admin_webweekly0.5https://centrifugal.dev/docs/3/server/authenticationweekly0.5https://centrifugal.dev/docs/3/server/channelsweekly0.5https://centrifugal.dev/docs/3/server/codesweekly0.5https://centrifugal.dev/docs/3/server/configurationweekly0.5https://centrifugal.dev/docs/3/server/console_commandsweekly0.5https://centrifugal.dev/docs/3/server/enginesweekly0.5https://centrifugal.dev/docs/3/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/3/server/infra_tuningweekly0.5https://centrifugal.dev/docs/3/server/load_balancingweekly0.5https://centrifugal.dev/docs/3/server/monitoringweekly0.5https://centrifugal.dev/docs/3/server/private_channelsweekly0.5https://centrifugal.dev/docs/3/server/proxyweekly0.5https://centrifugal.dev/docs/3/server/server_apiweekly0.5https://centrifugal.dev/docs/3/server/server_subsweekly0.5https://centrifugal.dev/docs/3/server/tlsweekly0.5https://centrifugal.dev/docs/3/transports/client_protocolweekly0.5https://centrifugal.dev/docs/3/transports/client_sdkweekly0.5https://centrifugal.dev/docs/3/transports/overviewweekly0.5https://centrifugal.dev/docs/3/transports/sockjsweekly0.5https://centrifugal.dev/docs/3/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/3/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/3/transports/uni_sseweekly0.5https://centrifugal.dev/docs/3/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/3/transports/websocketweekly0.5https://centrifugal.dev/docs/4/attributionsweekly0.5https://centrifugal.dev/docs/4/faqweekly0.5https://centrifugal.dev/docs/4/flow_diagramsweekly0.5https://centrifugal.dev/docs/4/getting-started/client_apiweekly0.5https://centrifugal.dev/docs/4/getting-started/communityweekly0.5https://centrifugal.dev/docs/4/getting-started/designweekly0.5https://centrifugal.dev/docs/4/getting-started/ecosystemweekly0.5https://centrifugal.dev/docs/4/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/4/getting-started/installationweekly0.5https://centrifugal.dev/docs/4/getting-started/integrationweekly0.5https://centrifugal.dev/docs/4/getting-started/introductionweekly0.5https://centrifugal.dev/docs/4/getting-started/migration_v4weekly0.5https://centrifugal.dev/docs/4/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/4/pro/analyticsweekly0.5https://centrifugal.dev/docs/4/pro/capabilitiesweekly0.5https://centrifugal.dev/docs/4/pro/cel_expressionsweekly0.5https://centrifugal.dev/docs/4/pro/channel_patternsweekly0.5https://centrifugal.dev/docs/4/pro/client_message_batchingweekly0.5https://centrifugal.dev/docs/4/pro/connectionsweekly0.5https://centrifugal.dev/docs/4/pro/install_and_runweekly0.5https://centrifugal.dev/docs/4/pro/overviewweekly0.5https://centrifugal.dev/docs/4/pro/performanceweekly0.5https://centrifugal.dev/docs/4/pro/process_statsweekly0.5https://centrifugal.dev/docs/4/pro/push_notificationsweekly0.5https://centrifugal.dev/docs/4/pro/singleflightweekly0.5https://centrifugal.dev/docs/4/pro/throttlingweekly0.5https://centrifugal.dev/docs/4/pro/token_revocationweekly0.5https://centrifugal.dev/docs/4/pro/tracingweekly0.5https://centrifugal.dev/docs/4/pro/user_blockweekly0.5https://centrifugal.dev/docs/4/pro/user_statusweekly0.5https://centrifugal.dev/docs/4/server/admin_webweekly0.5https://centrifugal.dev/docs/4/server/authenticationweekly0.5https://centrifugal.dev/docs/4/server/channel_permissionsweekly0.5https://centrifugal.dev/docs/4/server/channel_token_authweekly0.5https://centrifugal.dev/docs/4/server/channelsweekly0.5https://centrifugal.dev/docs/4/server/codesweekly0.5https://centrifugal.dev/docs/4/server/configurationweekly0.5https://centrifugal.dev/docs/4/server/console_commandsweekly0.5https://centrifugal.dev/docs/4/server/enginesweekly0.5https://centrifugal.dev/docs/4/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/4/server/infra_tuningweekly0.5https://centrifugal.dev/docs/4/server/load_balancingweekly0.5https://centrifugal.dev/docs/4/server/monitoringweekly0.5https://centrifugal.dev/docs/4/server/presenceweekly0.5https://centrifugal.dev/docs/4/server/proxyweekly0.5https://centrifugal.dev/docs/4/server/server_apiweekly0.5https://centrifugal.dev/docs/4/server/server_subsweekly0.5https://centrifugal.dev/docs/4/server/tlsweekly0.5https://centrifugal.dev/docs/4/transports/client_apiweekly0.5https://centrifugal.dev/docs/4/transports/client_protocolweekly0.5https://centrifugal.dev/docs/4/transports/client_sdkweekly0.5https://centrifugal.dev/docs/4/transports/http_streamweekly0.5https://centrifugal.dev/docs/4/transports/overviewweekly0.5https://centrifugal.dev/docs/4/transports/sockjsweekly0.5https://centrifugal.dev/docs/4/transports/sseweekly0.5https://centrifugal.dev/docs/4/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/4/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/4/transports/uni_sseweekly0.5https://centrifugal.dev/docs/4/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/4/transports/websocketweekly0.5https://centrifugal.dev/docs/4/transports/webtransportweekly0.5https://centrifugal.dev/docs/attributionsweekly0.5https://centrifugal.dev/docs/faqweekly0.5https://centrifugal.dev/docs/flow_diagramsweekly0.5https://centrifugal.dev/docs/getting-started/communityweekly0.5https://centrifugal.dev/docs/getting-started/comparisonsweekly0.5https://centrifugal.dev/docs/getting-started/designweekly0.5https://centrifugal.dev/docs/getting-started/ecosystemweekly0.5https://centrifugal.dev/docs/getting-started/highlightsweekly0.5https://centrifugal.dev/docs/getting-started/installationweekly0.5https://centrifugal.dev/docs/getting-started/integrationweekly0.5https://centrifugal.dev/docs/getting-started/introductionweekly0.5https://centrifugal.dev/docs/getting-started/migration_v4weekly0.5https://centrifugal.dev/docs/getting-started/migration_v5weekly0.5https://centrifugal.dev/docs/getting-started/quickstartweekly0.5https://centrifugal.dev/docs/pro/admin_idp_authweekly0.5https://centrifugal.dev/docs/pro/analyticsweekly0.5https://centrifugal.dev/docs/pro/capabilitiesweekly0.5https://centrifugal.dev/docs/pro/cel_expressionsweekly0.5https://centrifugal.dev/docs/pro/channel_cache_emptyweekly0.5https://centrifugal.dev/docs/pro/channel_patternsweekly0.5https://centrifugal.dev/docs/pro/channel_state_eventsweekly0.5https://centrifugal.dev/docs/pro/client_message_batchingweekly0.5https://centrifugal.dev/docs/pro/connectionsweekly0.5https://centrifugal.dev/docs/pro/delta_at_most_onceweekly0.5https://centrifugal.dev/docs/pro/distributed_rate_limitweekly0.5https://centrifugal.dev/docs/pro/engine_optimizationsweekly0.5https://centrifugal.dev/docs/pro/install_and_runweekly0.5https://centrifugal.dev/docs/pro/observability_enhancementsweekly0.5https://centrifugal.dev/docs/pro/overviewweekly0.5https://centrifugal.dev/docs/pro/performanceweekly0.5https://centrifugal.dev/docs/pro/process_statsweekly0.5https://centrifugal.dev/docs/pro/push_notificationsweekly0.5https://centrifugal.dev/docs/pro/rate_limitingweekly0.5https://centrifugal.dev/docs/pro/token_revocationweekly0.5https://centrifugal.dev/docs/pro/tracingweekly0.5https://centrifugal.dev/docs/pro/user_blockweekly0.5https://centrifugal.dev/docs/pro/user_statusweekly0.5https://centrifugal.dev/docs/server/admin_webweekly0.5https://centrifugal.dev/docs/server/authenticationweekly0.5https://centrifugal.dev/docs/server/cache_recoveryweekly0.5https://centrifugal.dev/docs/server/channel_permissionsweekly0.5https://centrifugal.dev/docs/server/channel_token_authweekly0.5https://centrifugal.dev/docs/server/channelsweekly0.5https://centrifugal.dev/docs/server/codesweekly0.5https://centrifugal.dev/docs/server/configurationweekly0.5https://centrifugal.dev/docs/server/console_commandsweekly0.5https://centrifugal.dev/docs/server/consumersweekly0.5https://centrifugal.dev/docs/server/delta_compressionweekly0.5https://centrifugal.dev/docs/server/enginesweekly0.5https://centrifugal.dev/docs/server/history_and_recoveryweekly0.5https://centrifugal.dev/docs/server/infra_tuningweekly0.5https://centrifugal.dev/docs/server/load_balancingweekly0.5https://centrifugal.dev/docs/server/monitoringweekly0.5https://centrifugal.dev/docs/server/observabilityweekly0.5https://centrifugal.dev/docs/server/presenceweekly0.5https://centrifugal.dev/docs/server/proxyweekly0.5https://centrifugal.dev/docs/server/proxy_streamsweekly0.5https://centrifugal.dev/docs/server/server_apiweekly0.5https://centrifugal.dev/docs/server/server_subsweekly0.5https://centrifugal.dev/docs/server/tlsweekly0.5https://centrifugal.dev/docs/transports/client_apiweekly0.5https://centrifugal.dev/docs/transports/client_protocolweekly0.5https://centrifugal.dev/docs/transports/client_sdkweekly0.5https://centrifugal.dev/docs/transports/http_streamweekly0.5https://centrifugal.dev/docs/transports/overviewweekly0.5https://centrifugal.dev/docs/transports/sockjsweekly0.5https://centrifugal.dev/docs/transports/sseweekly0.5https://centrifugal.dev/docs/transports/uni_client_protocolweekly0.5https://centrifugal.dev/docs/transports/uni_grpcweekly0.5https://centrifugal.dev/docs/transports/uni_http_streamweekly0.5https://centrifugal.dev/docs/transports/uni_sseweekly0.5https://centrifugal.dev/docs/transports/uni_websocketweekly0.5https://centrifugal.dev/docs/transports/websocketweekly0.5https://centrifugal.dev/docs/transports/webtransportweekly0.5https://centrifugal.dev/docs/tutorial/backendweekly0.5https://centrifugal.dev/docs/tutorial/centrifugoweekly0.5https://centrifugal.dev/docs/tutorial/frontendweekly0.5https://centrifugal.dev/docs/tutorial/improvementsweekly0.5https://centrifugal.dev/docs/tutorial/introweekly0.5https://centrifugal.dev/docs/tutorial/layoutweekly0.5https://centrifugal.dev/docs/tutorial/outbox_cdcweekly0.5https://centrifugal.dev/docs/tutorial/outroweekly0.5https://centrifugal.dev/docs/tutorial/recoveryweekly0.5https://centrifugal.dev/docs/tutorial/reverse_proxyweekly0.5https://centrifugal.dev/docs/tutorial/scaleweekly0.5https://centrifugal.dev/docs/tutorial/tips_and_tricksweekly0.5https://centrifugal.dev/weekly0.5 \ No newline at end of file