From 07778d930c7aac438cc5dd7b4c6a984ca02f6b2b Mon Sep 17 00:00:00 2001 From: Alexander Emelin Date: Thu, 20 Jun 2024 18:04:08 +0300 Subject: [PATCH] Deploy website - based on b2c2521e561545ddeba52c39aed297fd8bcf1c2c --- .DS_Store | Bin 8196 -> 6148 bytes 404.html | 4 ++-- assets/js/005b200e.2564fc33.js | 1 - assets/js/005b200e.a436342a.js | 1 + ...6d6f0.a687bcf1.js => 0136d6f0.bf4e4e2d.js} | 2 +- assets/js/01a85c17.0c8a856b.js | 1 - assets/js/01a85c17.2f7dfa16.js | 1 + ...ca2db.56b499f1.js => 01eca2db.000207f1.js} | 2 +- ...c3258.dc3fa22f.js => 04ac3258.d14e9138.js} | 2 +- ...9ead7.6851e4ba.js => 06f9ead7.c5420918.js} | 2 +- ...82599.d2d33efd.js => 09382599.503acccd.js} | 2 +- ...03bfe.a27685cc.js => 0d503bfe.7ffec748.js} | 2 +- ...7d15e.8271dc55.js => 0d57d15e.48ceaaaa.js} | 2 +- ...36dc4.5a84cb5d.js => 0dc36dc4.972ce2f0.js} | 2 +- assets/js/0eae5577.40a9360f.js | 1 + assets/js/0eae5577.ca7cde91.js | 1 - assets/js/1248e41e.566dbeb1.js | 1 - assets/js/1248e41e.8effc9e6.js | 1 + assets/js/1343.39ce3873.js | 1 - assets/js/1772.6d5a31b9.js | 1 + assets/js/17896441.2a0b9e89.js | 1 + assets/js/17896441.f5808a1a.js | 1 - ...93598.dc1cbbe0.js => 18793598.d6ba6946.js} | 2 +- ...a8b1e.68037fd1.js => 192a8b1e.55937a32.js} | 2 +- assets/js/19e7756f.60e8dc46.js | 1 + assets/js/19e7756f.731b1131.js | 1 - assets/js/1a4e3797.7620272a.js | 2 -- assets/js/1a4e3797.9c47db27.js | 2 ++ ...E.txt => 1a4e3797.9c47db27.js.LICENSE.txt} | 0 ...70467.94579b78.js => 1cd70467.54c40b64.js} | 2 +- ...c9151.84a6d2bd.js => 1d3c9151.14f00664.js} | 2 +- ...d4d48.240887d6.js => 1d4d4d48.7e48f500.js} | 2 +- assets/js/1f391b9e.611ab5a0.js | 1 - assets/js/1f391b9e.a3a4dbd2.js | 1 + ...4d804.3c831ba1.js => 20c4d804.9ada3512.js} | 2 +- assets/js/211f1d7a.116dc560.js | 1 - assets/js/211f1d7a.afce88de.js | 1 + assets/js/2312.a6b4f4c3.js | 1 + ...1cf3d.0da3dbbb.js => 2391cf3d.558435a6.js} | 2 +- ...a22d2.fac79ce0.js => 267a22d2.0b07620a.js} | 2 +- assets/js/2a42cb18.39f6beee.js | 1 - assets/js/2a42cb18.543d524d.js | 1 + ...47458.7064fa13.js => 2b147458.a382d9a1.js} | 2 +- ...f7ee0.980ae824.js => 2dbf7ee0.6722c7f6.js} | 2 +- ...54b47.fb6d8c31.js => 2e854b47.3bd3e607.js} | 2 +- ...9c429.95a18056.js => 2eb9c429.313fb402.js} | 2 +- assets/js/2f70c421.08296a05.js | 1 - assets/js/2f70c421.f5b43c19.js | 1 + ...aea06.5426b854.js => 369aea06.da51eb5e.js} | 2 +- ...4d18a.bff5a9e8.js => 39d4d18a.40345bfc.js} | 2 +- ...ce571.fc9e7f55.js => 3a2ce571.c475ab47.js} | 2 +- ...1ccb2.57855bec.js => 3c51ccb2.a857b5f6.js} | 2 +- ...8d52f.ee202a05.js => 4268d52f.929a6832.js} | 2 +- ...12ebf.753e03a1.js => 49012ebf.82ed41cd.js} | 2 +- assets/js/4ebb2955.65567078.js | 1 - assets/js/4ebb2955.c54e9f74.js | 1 + ...37bcb.3b8ebc2a.js => 4ec37bcb.3cc9f87e.js} | 2 +- assets/js/5072.1c9b2ea9.js | 1 - assets/js/5386.d06b5c39.js | 1 + ...31886.0d3d77fe.js => 56231886.43d386d4.js} | 2 +- assets/js/58b29436.2a825acd.js | 1 - assets/js/58b29436.4d63dfd7.js | 1 + assets/js/5de4a79c.933cba1e.js | 1 + assets/js/5de4a79c.dd90b916.js | 1 - assets/js/5e95c892.440c495a.js | 1 + assets/js/5e95c892.de90d7cd.js | 1 - assets/js/6230.facbdd8c.js | 1 - ...b5641.b2face4a.js => 629b5641.8d13fd23.js} | 2 +- assets/js/6875c492.88e01907.js | 1 + assets/js/6875c492.dcd4752c.js | 1 - ...24bd3.30cd319b.js => 6aa24bd3.b50e7e32.js} | 2 +- ...e284c.39e4d39a.js => 6fbe284c.fbf4295d.js} | 2 +- ...a60b8.a743c33e.js => 70aa60b8.3be1d920.js} | 2 +- assets/js/7672fb2a.5484757a.js | 1 + assets/js/7672fb2a.7e896a5e.js | 1 - ...d934d.1a4afaae.js => 776d934d.d624e353.js} | 2 +- assets/js/7bd30152.0eea2ce3.js | 1 + assets/js/7bd30152.8e165ca0.js | 1 - ...12894.a6333a8c.js => 81e12894.3fe3417c.js} | 2 +- ...30ab7.ec363c0c.js => 86030ab7.db43d3e9.js} | 2 +- ...c0c66.7d097db1.js => 984c0c66.2dcf5a91.js} | 2 +- ...0d0cc.6296ad3f.js => 9b70d0cc.4829c1a7.js} | 2 +- ...e219e.e7b09436.js => 9b9e219e.38fcac3d.js} | 2 +- ...ee1d6.338f9099.js => 9c1ee1d6.3cf0b566.js} | 2 +- ...b1acf.5025df44.js => 9c3b1acf.1570fddf.js} | 2 +- ...8a0d2.51584652.js => 9dd8a0d2.a0bac9de.js} | 2 +- assets/js/9e4087bc.85e787d3.js | 1 + assets/js/9e4087bc.a2bc1734.js | 1 - ...4038f.63c7cb3c.js => 9ff4038f.e09db7c7.js} | 2 +- ...a0a70.0c89101e.js => a41a0a70.eeb9c737.js} | 2 +- assets/js/a6aa9e1f.3d295b69.js | 1 - assets/js/a6aa9e1f.f2fb7c4e.js | 1 + ...df3cd.3ed923ae.js => a74df3cd.0d729f77.js} | 2 +- ...d4aaa.725e9150.js => a7bd4aaa.85343870.js} | 2 +- ...fa8b7.c43da8d6.js => a82fa8b7.17c3f2fe.js} | 2 +- assets/js/a94703ab.2997a41f.js | 1 + assets/js/a94703ab.de32f39f.js | 1 - ...f12ff.30ef4902.js => ab6f12ff.bca9b644.js} | 2 +- ...011d9.afd25068.js => b05011d9.3a93e9ff.js} | 2 +- ...0bebf.149f5dab.js => b4f0bebf.5eb4aa35.js} | 2 +- ...47432.9e412743.js => b5547432.f6cd5008.js} | 2 +- assets/js/b6f2a3eb.0aafea3b.js | 1 - assets/js/b6f2a3eb.49e44458.js | 1 + ...d3b30.550e1aad.js => ba0d3b30.47027545.js} | 2 +- ...9e52d.96978235.js => bbb9e52d.c0b7e715.js} | 2 +- ...14fff.6bb9d4e6.js => bbd14fff.78600d55.js} | 2 +- ...c395a.377d064a.js => be4c395a.3744e319.js} | 2 +- ...feea3.a9c63372.js => bfbfeea3.694435cb.js} | 2 +- ...8ab3c.d0ddbd28.js => c318ab3c.f2fa822e.js} | 2 +- ...80abd.7c07e02a.js => c8380abd.3f686e37.js} | 2 +- ...3329e.7b7237d1.js => c9a3329e.e66f5164.js} | 2 +- assets/js/ccc49370.88b4e60f.js | 1 + assets/js/ccc49370.c82c4b21.js | 1 - ...7a4f7.b898b54b.js => d1c7a4f7.12a972b8.js} | 2 +- assets/js/d2fe6fea.0bb4ff09.js | 1 - assets/js/d2fe6fea.33d8c26c.js | 1 + ...fc5db.bbf0386e.js => d4dfc5db.b91ad9a6.js} | 2 +- ...27831.e58e2507.js => d6627831.ec397b81.js} | 2 +- ...29201.790508c0.js => d9829201.f6796d5a.js} | 2 +- ...f115c.006a5139.js => db2f115c.c4dbaac4.js} | 2 +- ...faea1.d6ad8ef8.js => e66faea1.816a3587.js} | 2 +- ...93f84.3c004445.js => e7893f84.45b824e6.js} | 2 +- ...bd346.f5d6d6f5.js => e9cbd346.8d62a9dd.js} | 2 +- ...08d2f.3d7d9e1e.js => ea108d2f.897ca3e3.js} | 2 +- ...7a7af.9a4c588b.js => f1b7a7af.67edf125.js} | 2 +- ...176d2.ae9e2617.js => f26176d2.1aff2ccc.js} | 2 +- ...87449.b44217df.js => f8e87449.8a0967bf.js} | 2 +- assets/js/fbd7a87c.56152730.js | 1 - assets/js/fbd7a87c.cc490564.js | 1 + ...209d2.4188805c.js => fd3209d2.56c548a6.js} | 2 +- ...3cfee.e8a808c3.js => fd93cfee.c7d91946.js} | 2 +- ...1fc6f.b0f8ee9a.js => fe91fc6f.0269806d.js} | 2 +- ...4321a.261a30a0.js => ff64321a.e970fd74.js} | 2 +- ...d667d.2761806a.js => ffdd667d.a5ec9072.js} | 2 +- assets/js/main.439c6ff0.js | 2 ++ ...CENSE.txt => main.439c6ff0.js.LICENSE.txt} | 0 assets/js/main.ed6aad65.js | 2 -- ...n.1c2b1aba.js => runtime~main.294b73ca.js} | 2 +- blog.html | 4 ++-- .../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 | 4 ++-- .../06/03/real-time-document-state-sync.html | 4 ++-- blog/archive.html | 4 ++-- blog/tags.html | 4 ++-- blog/tags/authentication.html | 4 ++-- blog/tags/benthos.html | 4 ++-- blog/tags/centrifuge.html | 4 ++-- blog/tags/centrifugo.html | 4 ++-- blog/tags/compression.html | 4 ++-- blog/tags/django.html | 4 ++-- blog/tags/docsync.html | 4 ++-- 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 | 4 ++-- 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 | 8 ++++---- 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 6148 -> 16388 bytes index.html | 4 ++-- license.html | 4 ++-- license_exchange_lemon.html | 4 ++-- search.html | 4 ++-- 394 files changed, 621 insertions(+), 621 deletions(-) delete mode 100644 assets/js/005b200e.2564fc33.js create mode 100644 assets/js/005b200e.a436342a.js rename assets/js/{0136d6f0.a687bcf1.js => 0136d6f0.bf4e4e2d.js} (97%) delete mode 100644 assets/js/01a85c17.0c8a856b.js create mode 100644 assets/js/01a85c17.2f7dfa16.js rename assets/js/{01eca2db.56b499f1.js => 01eca2db.000207f1.js} (98%) rename assets/js/{04ac3258.dc3fa22f.js => 04ac3258.d14e9138.js} (96%) rename assets/js/{06f9ead7.6851e4ba.js => 06f9ead7.c5420918.js} (99%) rename assets/js/{09382599.d2d33efd.js => 09382599.503acccd.js} (97%) rename assets/js/{0d503bfe.a27685cc.js => 0d503bfe.7ffec748.js} (97%) rename assets/js/{0d57d15e.8271dc55.js => 0d57d15e.48ceaaaa.js} (98%) rename assets/js/{0dc36dc4.5a84cb5d.js => 0dc36dc4.972ce2f0.js} (96%) create mode 100644 assets/js/0eae5577.40a9360f.js delete mode 100644 assets/js/0eae5577.ca7cde91.js delete mode 100644 assets/js/1248e41e.566dbeb1.js create mode 100644 assets/js/1248e41e.8effc9e6.js delete mode 100644 assets/js/1343.39ce3873.js create mode 100644 assets/js/1772.6d5a31b9.js create mode 100644 assets/js/17896441.2a0b9e89.js delete mode 100644 assets/js/17896441.f5808a1a.js rename assets/js/{18793598.dc1cbbe0.js => 18793598.d6ba6946.js} (98%) rename assets/js/{192a8b1e.68037fd1.js => 192a8b1e.55937a32.js} (99%) create mode 100644 assets/js/19e7756f.60e8dc46.js delete mode 100644 assets/js/19e7756f.731b1131.js delete mode 100644 assets/js/1a4e3797.7620272a.js create mode 100644 assets/js/1a4e3797.9c47db27.js rename assets/js/{1a4e3797.7620272a.js.LICENSE.txt => 1a4e3797.9c47db27.js.LICENSE.txt} (100%) rename assets/js/{1cd70467.94579b78.js => 1cd70467.54c40b64.js} (98%) rename assets/js/{1d3c9151.84a6d2bd.js => 1d3c9151.14f00664.js} (96%) rename assets/js/{1d4d4d48.240887d6.js => 1d4d4d48.7e48f500.js} (99%) delete mode 100644 assets/js/1f391b9e.611ab5a0.js create mode 100644 assets/js/1f391b9e.a3a4dbd2.js rename assets/js/{20c4d804.3c831ba1.js => 20c4d804.9ada3512.js} (99%) delete mode 100644 assets/js/211f1d7a.116dc560.js create mode 100644 assets/js/211f1d7a.afce88de.js create mode 100644 assets/js/2312.a6b4f4c3.js rename assets/js/{2391cf3d.0da3dbbb.js => 2391cf3d.558435a6.js} (99%) rename assets/js/{267a22d2.fac79ce0.js => 267a22d2.0b07620a.js} (99%) delete mode 100644 assets/js/2a42cb18.39f6beee.js create mode 100644 assets/js/2a42cb18.543d524d.js rename assets/js/{2b147458.7064fa13.js => 2b147458.a382d9a1.js} (59%) rename assets/js/{2dbf7ee0.980ae824.js => 2dbf7ee0.6722c7f6.js} (99%) rename assets/js/{2e854b47.fb6d8c31.js => 2e854b47.3bd3e607.js} (98%) rename assets/js/{2eb9c429.95a18056.js => 2eb9c429.313fb402.js} (98%) delete mode 100644 assets/js/2f70c421.08296a05.js create mode 100644 assets/js/2f70c421.f5b43c19.js rename assets/js/{369aea06.5426b854.js => 369aea06.da51eb5e.js} (96%) rename assets/js/{39d4d18a.bff5a9e8.js => 39d4d18a.40345bfc.js} (99%) rename assets/js/{3a2ce571.fc9e7f55.js => 3a2ce571.c475ab47.js} (84%) rename assets/js/{3c51ccb2.57855bec.js => 3c51ccb2.a857b5f6.js} (99%) rename assets/js/{4268d52f.ee202a05.js => 4268d52f.929a6832.js} (99%) rename assets/js/{49012ebf.753e03a1.js => 49012ebf.82ed41cd.js} (56%) delete mode 100644 assets/js/4ebb2955.65567078.js create mode 100644 assets/js/4ebb2955.c54e9f74.js rename assets/js/{4ec37bcb.3b8ebc2a.js => 4ec37bcb.3cc9f87e.js} (93%) delete mode 100644 assets/js/5072.1c9b2ea9.js create mode 100644 assets/js/5386.d06b5c39.js rename assets/js/{56231886.0d3d77fe.js => 56231886.43d386d4.js} (96%) delete mode 100644 assets/js/58b29436.2a825acd.js create mode 100644 assets/js/58b29436.4d63dfd7.js create mode 100644 assets/js/5de4a79c.933cba1e.js delete mode 100644 assets/js/5de4a79c.dd90b916.js create mode 100644 assets/js/5e95c892.440c495a.js delete mode 100644 assets/js/5e95c892.de90d7cd.js delete mode 100644 assets/js/6230.facbdd8c.js rename assets/js/{629b5641.b2face4a.js => 629b5641.8d13fd23.js} (99%) create mode 100644 assets/js/6875c492.88e01907.js delete mode 100644 assets/js/6875c492.dcd4752c.js rename assets/js/{6aa24bd3.30cd319b.js => 6aa24bd3.b50e7e32.js} (99%) rename assets/js/{6fbe284c.39e4d39a.js => 6fbe284c.fbf4295d.js} (99%) rename assets/js/{70aa60b8.a743c33e.js => 70aa60b8.3be1d920.js} (99%) create mode 100644 assets/js/7672fb2a.5484757a.js delete mode 100644 assets/js/7672fb2a.7e896a5e.js rename assets/js/{776d934d.1a4afaae.js => 776d934d.d624e353.js} (96%) create mode 100644 assets/js/7bd30152.0eea2ce3.js delete mode 100644 assets/js/7bd30152.8e165ca0.js rename assets/js/{81e12894.a6333a8c.js => 81e12894.3fe3417c.js} (58%) rename assets/js/{86030ab7.ec363c0c.js => 86030ab7.db43d3e9.js} (97%) rename assets/js/{984c0c66.7d097db1.js => 984c0c66.2dcf5a91.js} (99%) rename assets/js/{9b70d0cc.6296ad3f.js => 9b70d0cc.4829c1a7.js} (96%) rename assets/js/{9b9e219e.e7b09436.js => 9b9e219e.38fcac3d.js} (99%) rename assets/js/{9c1ee1d6.338f9099.js => 9c1ee1d6.3cf0b566.js} (99%) rename assets/js/{9c3b1acf.5025df44.js => 9c3b1acf.1570fddf.js} (98%) rename assets/js/{9dd8a0d2.51584652.js => 9dd8a0d2.a0bac9de.js} (98%) create mode 100644 assets/js/9e4087bc.85e787d3.js delete mode 100644 assets/js/9e4087bc.a2bc1734.js rename assets/js/{9ff4038f.63c7cb3c.js => 9ff4038f.e09db7c7.js} (97%) rename assets/js/{a41a0a70.0c89101e.js => a41a0a70.eeb9c737.js} (96%) delete mode 100644 assets/js/a6aa9e1f.3d295b69.js create mode 100644 assets/js/a6aa9e1f.f2fb7c4e.js rename assets/js/{a74df3cd.3ed923ae.js => a74df3cd.0d729f77.js} (99%) rename assets/js/{a7bd4aaa.725e9150.js => a7bd4aaa.85343870.js} (73%) rename assets/js/{a82fa8b7.c43da8d6.js => a82fa8b7.17c3f2fe.js} (98%) create mode 100644 assets/js/a94703ab.2997a41f.js delete mode 100644 assets/js/a94703ab.de32f39f.js rename assets/js/{ab6f12ff.30ef4902.js => ab6f12ff.bca9b644.js} (99%) rename assets/js/{b05011d9.afd25068.js => b05011d9.3a93e9ff.js} (99%) rename assets/js/{b4f0bebf.149f5dab.js => b4f0bebf.5eb4aa35.js} (97%) rename assets/js/{b5547432.9e412743.js => b5547432.f6cd5008.js} (58%) delete mode 100644 assets/js/b6f2a3eb.0aafea3b.js create mode 100644 assets/js/b6f2a3eb.49e44458.js rename assets/js/{ba0d3b30.550e1aad.js => ba0d3b30.47027545.js} (98%) rename assets/js/{bbb9e52d.96978235.js => bbb9e52d.c0b7e715.js} (66%) rename assets/js/{bbd14fff.6bb9d4e6.js => bbd14fff.78600d55.js} (97%) rename assets/js/{be4c395a.377d064a.js => be4c395a.3744e319.js} (97%) rename assets/js/{bfbfeea3.a9c63372.js => bfbfeea3.694435cb.js} (99%) rename assets/js/{c318ab3c.d0ddbd28.js => c318ab3c.f2fa822e.js} (98%) rename assets/js/{c8380abd.7c07e02a.js => c8380abd.3f686e37.js} (97%) rename assets/js/{c9a3329e.7b7237d1.js => c9a3329e.e66f5164.js} (98%) create mode 100644 assets/js/ccc49370.88b4e60f.js delete mode 100644 assets/js/ccc49370.c82c4b21.js rename assets/js/{d1c7a4f7.b898b54b.js => d1c7a4f7.12a972b8.js} (99%) delete mode 100644 assets/js/d2fe6fea.0bb4ff09.js create mode 100644 assets/js/d2fe6fea.33d8c26c.js rename assets/js/{d4dfc5db.bbf0386e.js => d4dfc5db.b91ad9a6.js} (98%) rename assets/js/{d6627831.e58e2507.js => d6627831.ec397b81.js} (94%) rename assets/js/{d9829201.790508c0.js => d9829201.f6796d5a.js} (96%) rename assets/js/{db2f115c.006a5139.js => db2f115c.c4dbaac4.js} (98%) rename assets/js/{e66faea1.d6ad8ef8.js => e66faea1.816a3587.js} (96%) rename assets/js/{e7893f84.3c004445.js => e7893f84.45b824e6.js} (97%) rename assets/js/{e9cbd346.f5d6d6f5.js => e9cbd346.8d62a9dd.js} (60%) rename assets/js/{ea108d2f.3d7d9e1e.js => ea108d2f.897ca3e3.js} (99%) rename assets/js/{f1b7a7af.9a4c588b.js => f1b7a7af.67edf125.js} (99%) rename assets/js/{f26176d2.ae9e2617.js => f26176d2.1aff2ccc.js} (99%) rename assets/js/{f8e87449.b44217df.js => f8e87449.8a0967bf.js} (84%) delete mode 100644 assets/js/fbd7a87c.56152730.js create mode 100644 assets/js/fbd7a87c.cc490564.js rename assets/js/{fd3209d2.4188805c.js => fd3209d2.56c548a6.js} (96%) rename assets/js/{fd93cfee.e8a808c3.js => fd93cfee.c7d91946.js} (95%) rename assets/js/{fe91fc6f.b0f8ee9a.js => fe91fc6f.0269806d.js} (95%) rename assets/js/{ff64321a.261a30a0.js => ff64321a.e970fd74.js} (51%) rename assets/js/{ffdd667d.2761806a.js => ffdd667d.a5ec9072.js} (99%) create mode 100644 assets/js/main.439c6ff0.js rename assets/js/{main.ed6aad65.js.LICENSE.txt => main.439c6ff0.js.LICENSE.txt} (100%) delete mode 100644 assets/js/main.ed6aad65.js rename assets/js/{runtime~main.1c2b1aba.js => runtime~main.294b73ca.js} (62%) diff --git a/.DS_Store b/.DS_Store index 661934f49ae3c72708c9d147a3ce233ab955b66c..277fc958a54dcb6ba53d7a80b3cb5193aedaab50 100644 GIT binary patch delta 486 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50D9jF$2aCCS=Hw?Q<>V)AEL_Q0 z4-#i(aAxphaAfdhfXhL(I|6CF|6l-QF)%PQWHRJ3q{B=AihwLx55!nhC6yN!fb0Rv z?no-g$t*50Fu2CZ#LU9V#?Hac!OamHoH5x-AW~hTy4uoQN5RP0s8&az+R(_*Oh>`g z)S$MOlS5Ql-#REhJ0~|UziaYt0r~oFplMK7lm!>%<>cq314V#7W8eb1Esvp?p@bok zAqNQcfb0xZmokEMBfFFjO;0>S0Yec(K32W5AiY*dfx!hw&zll Baw7l$ literal 8196 zcmeHMTWl0n7(U;$z>FQ}v=k}JRu`&VH?oU@7F3qqf?TSFZc9r+mffA9OqkA;o!Ko= z8ykJ(;+x{-1&taLTb}uo*$E%6i8|bLH^7gDp*c|MyCryldMBQ zn1L_@VFtnsgc%4ka4Teh-r2m#H+k<1ZPey6jtL|UH3(Ni;fi330pU*aSYVe5a7>_ZX9zYQ2v$b0LxI0K%^&m4 z8BzknKFmOvfyo)*T8n@0Hzj zzO;jnx*f+VRL}E;ZKg4t)0#4l?d46|DESViZji~&Uenf{;j~k>b=UXJ3Wco5N=}v73G9Fvm_R#34BF}1Fwz?~~&l|}NW&b9UoX@sSH$CTyvWqJ< z)2^6AXS!d4)pBd<*l#|gT2qSzvs$YJ^TTdUCB#^v*HsH!&e~ZA+s#UBgdJta*$MVO zJHAxo zum=MeLInqK5Qp$A4&x|};W-?~3A~Ee@HRfe$M^)F;tbB>9LDhtzQsjc!jJd`zv8l_ zNR84=sYzNWEs~PbDk&pvkhY3Tm#VeXUkxpgcM%4U0;-+hQ=atljcX^m{;>@kH{HUu zbAE=XmRWbsj`0$D@Zr=Iwh4Mx8F?+w{kwc!N%-XUt7!r;$El{~MqAXot0gzF6nm&D zi~}++LT#F`p0C~`Q&nhF#BiaySf+Z=rix)adY?=cs7d}%O-AomsLr$+F^os+s7m;v zYMZ)Rp?cG5#jr(PqbM5l2hnu2U8b7Rrk zcn>FW3LoM$KEszp>GSw*0%1F@CoI0dO4v%aWLu6sK$0rGuQv`ylQU2wM9qBvzwYMW z|0g?!k}v~d25y@Spte2Ro~Cea_UPn$?HC=q>EMmqjS2K!sPnhug#LD%@X8;CbRHv9 k?vn~|Odv_9{pTM7=+|cW{^!5p|KX3>@cr-e^mgn12HC_*D*ylh diff --git a/404.html b/404.html index 5835c184d..67912120c 100644 --- a/404.html +++ b/404.html @@ -15,8 +15,8 @@ - - + +

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/005b200e.2564fc33.js b/assets/js/005b200e.2564fc33.js deleted file mode 100644 index e5d6c00f8..000000000 --- a/assets/js/005b200e.2564fc33.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9678],{11296:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>c});var i=n(85893),t=n(11151);const a={id:"delta_compression",sidebar_label:"Delta compression",title:"Delta compression in channels"},o=void 0,r={id:"server/delta_compression",title:"Delta compression in channels",description:"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.",source:"@site/docs/server/delta_compression.md",sourceDirName:"server",slug:"/server/delta_compression",permalink:"/docs/server/delta_compression",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/delta_compression.md",tags:[],version:"current",frontMatter:{id:"delta_compression",sidebar_label:"Delta compression",title:"Delta compression in channels"},sidebar:"Guides",previous:{title:"Cache recovery mode",permalink:"/docs/server/cache_recovery"},next:{title:"Online presence",permalink:"/docs/server/presence"}},l={},c=[{value:"Subscribe using delta",id:"subscribe-using-delta",level:3},{value:"Use delta when publishing",id:"use-delta-when-publishing",level:3}];function d(e){const s={a:"a",admonition:"admonition",code:"code",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(s.p,{children:["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 ",(0,i.jsx)(s.strong,{children:"since Centrifugo v5.4.0"}),"."]}),"\n",(0,i.jsx)(s.p,{children:"Delta compression is beneficial for channels that send a series of updates to a particular object or document with high similarity between successive publications. A client can apply the delta to the previous message to reconstruct the full payload."}),"\n",(0,i.jsxs)(s.p,{children:["Using delta mode can significantly reduce the size of each message when the differences between successive payloads are small compared to their overall size. This reduction ",(0,i.jsx)(s.strong,{children:"can lower bandwidth costs"}),", decrease transit latencies, and increase message throughput on a connection."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.img,{alt:"delta frames",src:n(44923).Z+"",width:"4002",height:"902"})}),"\n",(0,i.jsx)(s.p,{children:"In the scenario we used to evaluate delta compression feature usefullness we were able to achieve x10 reduction of traffic going through the network interface by enabling delta compression in the channel. This heavily depends on the nature of data you publish, but proves that deltas make a perfect sense in some scenarios."}),"\n",(0,i.jsxs)(s.p,{children:["The diff is calculated using ",(0,i.jsx)(s.a,{href:"https://fossil-scm.org/home/doc/tip/www/delta_format.wiki",children:"Fossil"})," delta algorithm. Delta compression via Fossil supports all payloads, whether binary, or JSON-encoded. The delta algorithm processes message payloads as opaque binaries and has no dependency on the structure of the payload."]}),"\n",(0,i.jsx)(s.admonition,{type:"tip",children:(0,i.jsxs)(s.p,{children:["At this point delta compression is only available for bidirectional client-side subscriptions and supported by Centrifugo Javascript SDK ",(0,i.jsx)(s.a,{href:"https://github.com/centrifugal/centrifuge-js",children:"centrifuge-js"})," (since 5.2.0)."]})}),"\n",(0,i.jsxs)(s.p,{children:["Deltas apply only to the ",(0,i.jsx)(s.code,{children:"data"})," property of a Publication. Publications retrieved via history calls are not compressed \u2013 delta applied only for clent protocol publications travelling to real-time connections."]}),"\n",(0,i.jsx)(s.p,{children:"How it may look in practice? Here is a screenshot of WebSocket frames in case of using our JSON protocol format. Note that the connection receives publication push with full payload first, then only deltas are sent which are much smaller in size:"}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.img,{alt:"delta frames",src:n(87936).Z+"",width:"2828",height:"572"})}),"\n",(0,i.jsx)(s.h3,{id:"subscribe-using-delta",children:"Subscribe using delta"}),"\n",(0,i.jsx)(s.p,{children:"To successfully negotiate delta compression for a subscriber several conditions should be met:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["subscriber provides ",(0,i.jsx)(s.code,{children:'delta: "fossil"'})," option when creating a client-side Subscription"]}),"\n",(0,i.jsxs)(s.li,{children:["server uses ",(0,i.jsx)(s.code,{children:'"allowed_delta_types": ["fossil"]'})," for a channel namespace a client subscribes to"]}),"\n",(0,i.jsx)(s.li,{children:"server uses history for a channel"}),"\n",(0,i.jsx)(s.li,{children:"positioning or recovery are used for channel subscription"}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"Example of subscription creation on the client side:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('example:updates', {\n delta: 'fossil'\n});\n"})}),"\n",(0,i.jsx)(s.p,{children:"And the example of Centrifugo configuration:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ..\n "namespaces": [\n {\n "name": "example",\n "allowed_delta_types": ["fossil"],\n "force_positioning": true,\n "history_size": 1,\n "history_ttl": "60s"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(s.admonition,{type:"tip",children:(0,i.jsxs)(s.p,{children:["If you want to use delta compression without history, positioning and recovery on, i.e. in at most once scenario \u2013 then Centrifugo PRO ",(0,i.jsx)(s.a,{href:"/docs/pro/delta_at_most_once",children:"provides such a possibility"})," with its option to keep latest publication in channel in the node's memory."]})}),"\n",(0,i.jsx)(s.p,{children:"If all conditions met \u2013 subscriber will negotiate compression with a server. If SDK does not support delta compression \u2013 it can still subscribe to the channel, but will receive publications with full payload. To let Centrifugo know that delta compression must be used for a particular publication some configuration is required for the publisher also. We will describe it shortly."}),"\n",(0,i.jsx)(s.h3,{id:"use-delta-when-publishing",children:"Use delta when publishing"}),"\n",(0,i.jsx)(s.p,{children:"If subscriber successfully negotiated delta compression with Centrifugo, it will start receiving deltas for publications marked with delta flag by the publisher. It's possible to mark channel publications to use delta compression upon broadcasting to subscribers in the following ways:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["enable it for all publications in the channel namespace by setting a boolean channel option ",(0,i.jsx)(s.a,{href:"/docs/server/channels#delta_publish",children:"delta_publish"})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"delta"})," flag may be set on a per call basis (in publish or broadcast server APIs). For example, see ",(0,i.jsx)(s.code,{children:"delta"})," field in ",(0,i.jsx)(s.a,{href:"/docs/server/server_api#publish-request",children:"publish request"})," description."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"For example, this means that to automatically use delta calculation for all publications in the namespace the configuration example above evolves to:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ..\n "namespaces": [\n {\n "name": "example",\n "allowed_delta_types": ["fossil"],\n "force_positioning": true,\n "history_size": 1,\n "history_ttl": "60s",\n "delta_publish": true\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(s.p,{children:"Again \u2013 subscribers which support delta compression and do not support it can co-exist in one channel."})]})}function h(e={}){const{wrapper:s}={...(0,t.a)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},44923:(e,s,n)=>{n.d(s,{Z:()=>i});const i=n.p+"assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png"},87936:(e,s,n)=>{n.d(s,{Z:()=>i});const i=n.p+"assets/images/delta_frames-7d915a6b62f3cbcbfa4e0a1d738e79df.png"},11151:(e,s,n)=>{n.d(s,{Z:()=>r,a:()=>o});var i=n(67294);const t={},a=i.createContext(t);function o(e){const s=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function r(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/005b200e.a436342a.js b/assets/js/005b200e.a436342a.js new file mode 100644 index 000000000..12dc57153 --- /dev/null +++ b/assets/js/005b200e.a436342a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9678],{11296:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>c});var i=n(85893),t=n(11151);const a={id:"delta_compression",sidebar_label:"Delta compression",title:"Delta compression in channels"},o=void 0,r={id:"server/delta_compression",title:"Delta compression in channels",description:"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.",source:"@site/docs/server/delta_compression.md",sourceDirName:"server",slug:"/server/delta_compression",permalink:"/docs/server/delta_compression",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/delta_compression.md",tags:[],version:"current",frontMatter:{id:"delta_compression",sidebar_label:"Delta compression",title:"Delta compression in channels"},sidebar:"Guides",previous:{title:"Cache recovery mode",permalink:"/docs/server/cache_recovery"},next:{title:"Online presence",permalink:"/docs/server/presence"}},l={},c=[{value:"Subscribe using delta",id:"subscribe-using-delta",level:3},{value:"Use delta when publishing",id:"use-delta-when-publishing",level:3}];function d(e){const s={a:"a",admonition:"admonition",code:"code",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(s.p,{children:["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 ",(0,i.jsx)(s.strong,{children:"since Centrifugo v5.4.0"}),"."]}),"\n",(0,i.jsx)(s.p,{children:"Delta compression is beneficial for channels that send a series of updates to a particular object or document with high similarity between successive publications. A client can apply the delta to the previous message to reconstruct the full payload."}),"\n",(0,i.jsxs)(s.p,{children:["Using delta mode can significantly reduce the size of each message when the differences between successive payloads are small compared to their overall size. This reduction ",(0,i.jsx)(s.strong,{children:"can lower bandwidth costs"}),", decrease transit latencies, and increase message throughput on a connection."]}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.img,{alt:"delta frames",src:n(85259).Z+"",width:"4002",height:"902"})}),"\n",(0,i.jsx)(s.p,{children:"In the scenario we used to evaluate delta compression feature usefullness we were able to achieve x10 reduction of traffic going through the network interface by enabling delta compression in the channel. This heavily depends on the nature of data you publish, but proves that deltas make a perfect sense in some scenarios."}),"\n",(0,i.jsxs)(s.p,{children:["The diff is calculated using ",(0,i.jsx)(s.a,{href:"https://fossil-scm.org/home/doc/tip/www/delta_format.wiki",children:"Fossil"})," delta algorithm. Delta compression via Fossil supports all payloads, whether binary, or JSON-encoded. The delta algorithm processes message payloads as opaque binaries and has no dependency on the structure of the payload."]}),"\n",(0,i.jsx)(s.admonition,{type:"tip",children:(0,i.jsxs)(s.p,{children:["At this point delta compression is only available for bidirectional client-side subscriptions and supported by Centrifugo Javascript SDK ",(0,i.jsx)(s.a,{href:"https://github.com/centrifugal/centrifuge-js",children:"centrifuge-js"})," (since 5.2.0)."]})}),"\n",(0,i.jsxs)(s.p,{children:["Deltas apply only to the ",(0,i.jsx)(s.code,{children:"data"})," property of a Publication. Publications retrieved via history calls are not compressed \u2013 delta applied only for clent protocol publications travelling to real-time connections."]}),"\n",(0,i.jsx)(s.p,{children:"How it may look in practice? Here is a screenshot of WebSocket frames in case of using our JSON protocol format. Note that the connection receives publication push with full payload first, then only deltas are sent which are much smaller in size:"}),"\n",(0,i.jsx)(s.p,{children:(0,i.jsx)(s.img,{alt:"delta frames",src:n(9857).Z+"",width:"2828",height:"572"})}),"\n",(0,i.jsx)(s.h3,{id:"subscribe-using-delta",children:"Subscribe using delta"}),"\n",(0,i.jsx)(s.p,{children:"To successfully negotiate delta compression for a subscriber several conditions should be met:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["subscriber provides ",(0,i.jsx)(s.code,{children:'delta: "fossil"'})," option when creating a client-side Subscription"]}),"\n",(0,i.jsxs)(s.li,{children:["server uses ",(0,i.jsx)(s.code,{children:'"allowed_delta_types": ["fossil"]'})," for a channel namespace a client subscribes to"]}),"\n",(0,i.jsx)(s.li,{children:"server uses history for a channel"}),"\n",(0,i.jsx)(s.li,{children:"positioning or recovery are used for channel subscription"}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"Example of subscription creation on the client side:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('example:updates', {\n delta: 'fossil'\n});\n"})}),"\n",(0,i.jsx)(s.p,{children:"And the example of Centrifugo configuration:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ..\n "namespaces": [\n {\n "name": "example",\n "allowed_delta_types": ["fossil"],\n "force_positioning": true,\n "history_size": 1,\n "history_ttl": "60s"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(s.admonition,{type:"tip",children:(0,i.jsxs)(s.p,{children:["If you want to use delta compression without history, positioning and recovery on, i.e. in at most once scenario \u2013 then Centrifugo PRO ",(0,i.jsx)(s.a,{href:"/docs/pro/delta_at_most_once",children:"provides such a possibility"})," with its option to keep latest publication in channel in the node's memory."]})}),"\n",(0,i.jsx)(s.p,{children:"If all conditions met \u2013 subscriber will negotiate compression with a server. If SDK does not support delta compression \u2013 it can still subscribe to the channel, but will receive publications with full payload. To let Centrifugo know that delta compression must be used for a particular publication some configuration is required for the publisher also. We will describe it shortly."}),"\n",(0,i.jsx)(s.h3,{id:"use-delta-when-publishing",children:"Use delta when publishing"}),"\n",(0,i.jsx)(s.p,{children:"If subscriber successfully negotiated delta compression with Centrifugo, it will start receiving deltas for publications marked with delta flag by the publisher. It's possible to mark channel publications to use delta compression upon broadcasting to subscribers in the following ways:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["enable it for all publications in the channel namespace by setting a boolean channel option ",(0,i.jsx)(s.a,{href:"/docs/server/channels#delta_publish",children:"delta_publish"})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"delta"})," flag may be set on a per call basis (in publish or broadcast server APIs). For example, see ",(0,i.jsx)(s.code,{children:"delta"})," field in ",(0,i.jsx)(s.a,{href:"/docs/server/server_api#publish-request",children:"publish request"})," description."]}),"\n"]}),"\n",(0,i.jsx)(s.p,{children:"For example, this means that to automatically use delta calculation for all publications in the namespace the configuration example above evolves to:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ..\n "namespaces": [\n {\n "name": "example",\n "allowed_delta_types": ["fossil"],\n "force_positioning": true,\n "history_size": 1,\n "history_ttl": "60s",\n "delta_publish": true\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(s.p,{children:"Again \u2013 subscribers which support delta compression and do not support it can co-exist in one channel."})]})}function h(e={}){const{wrapper:s}={...(0,t.a)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},85259:(e,s,n)=>{n.d(s,{Z:()=>i});const i=n.p+"assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png"},9857:(e,s,n)=>{n.d(s,{Z:()=>i});const i=n.p+"assets/images/delta_frames-7d915a6b62f3cbcbfa4e0a1d738e79df.png"},11151:(e,s,n)=>{n.d(s,{Z:()=>r,a:()=>o});var i=n(67294);const t={},a=i.createContext(t);function o(e){const s=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function r(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0136d6f0.a687bcf1.js b/assets/js/0136d6f0.bf4e4e2d.js similarity index 97% rename from assets/js/0136d6f0.a687bcf1.js rename to assets/js/0136d6f0.bf4e4e2d.js index 9a56bed3b..8a5c33ce4 100644 --- a/assets/js/0136d6f0.a687bcf1.js +++ b/assets/js/0136d6f0.bf4e4e2d.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1152],{9838:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});var i=n(85893),o=n(11151);const a={id:"introduction",title:"Centrifugo introduction"},s=void 0,r={id:"getting-started/introduction",title:"Centrifugo introduction",description:"Centrifugo is an open-source scalable real-time messaging server in a language-agnostic way.",source:"@site/versioned_docs/version-3/getting-started/introduction.md",sourceDirName:"getting-started",slug:"/getting-started/introduction",permalink:"/docs/3/getting-started/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/introduction.md",tags:[],version:"3",frontMatter:{id:"introduction",title:"Centrifugo introduction"},sidebar:"Introduction",next:{title:"Install Centrifugo",permalink:"/docs/3/getting-started/installation"}},c={},l=[{value:"Motivation",id:"motivation",level:2},{value:"Concepts",id:"concepts",level:2},{value:"Join community",id:"join-community",level:2}];function d(e){const t={a:"a",admonition:"admonition",h2:"h2",img:"img",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"Centrifugo is an open-source scalable real-time messaging server in a language-agnostic way."}),"\n",(0,i.jsx)(t.admonition,{title:"Real-time?",type:"info",children:(0,i.jsx)(t.p,{children:"By real-time, we indicate a soft real-time. Due to network latencies, garbage collection cycles, and so on, the delay of a delivered message can be up to several hundred milliseconds or higher."})}),"\n",(0,i.jsx)(t.p,{children:"It can be a missing piece in your application architecture to send real-time updates to users. Think chats, live comments, multiplayer games, streaming metrics \u2013 you'll be able to build amazing web and mobile real-time apps with a help of Centrifugo."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo works in conjunction with applications written in any programming language \u2013 both on the backend and frontend sides. It runs as a standalone service hosted on your hardware and fits well to both monolithic and microservice architectures."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is fast and scales well to support millions of concurrent client connections. It provides several real-time transports to choose from and a set of features to simplify building real-time applications."}),"\n",(0,i.jsx)(t.h2,{id:"motivation",children:"Motivation"}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo was born to help applications with a server-side written in a language or a framework without built-in concurrency support. In this case, dealing with persistent connections is a real headache that usually can only be resolved by introducing a shift in the technology stack and spending enough time to create a production-ready solution."}),"\n",(0,i.jsx)(t.p,{children:"For example, frameworks like Django, Flask, Yii, Laravel, Ruby on Rails, and others have poor and not performant support of working with many persistent connections for the real-time messaging task."}),"\n",(0,i.jsx)(t.p,{children:"In this case, Centrifugo is a very straightforward and non-obtrusive way to introduce real-time updates and handle lots of persistent connections without radical changes in application backend architecture. Developers could proceed writing a backend with a favorite language or favorite framework, keep existing architecture \u2013 and just let Centrifugo deal with persistent connections."}),"\n",(0,i.jsx)(t.p,{children:"At the moment, Centrifugo provides some advanced and unique features that can simplify a developer's life and save months of development, even if the application backend is built with the asynchronous concurrent language. One example is that Centrifugo can scale out-of-the-box to many machines with several supported brokers. And there are more things to mention \u2013 see detailed highlights further in the docs."}),"\n",(0,i.jsx)(t.h2,{id:"concepts",children:"Concepts"}),"\n",(0,i.jsx)(t.p,{children:"As mentioned above, Centrifugo runs as a standalone service that cares about handling persistent connections from application users. Application backend and frontend can be written in any programming language. Clients connect to Centrifugo and subscribe to channels."}),"\n",(0,i.jsx)(t.p,{children:"As soon as some event happens application backend can publish a message with event payload into a channel using Centrifugo API. The message will be delivered to all clients currently connected and subscribed to a channel."}),"\n",(0,i.jsx)(t.p,{children:"So Centrifugo is a user-facing PUB/SUB server in a nutshell. Here is a simplified scheme:"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Centrifugo scheme",src:n(88687).Z+"",width:"1186",height:"626"})}),"\n",(0,i.jsx)(t.h2,{id:"join-community",children:"Join community"}),"\n",(0,i.jsx)(t.p,{children:"We have rooms in Telegram and Discord:"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ",children:(0,i.jsx)(t.img,{src:"https://img.shields.io/badge/Telegram-Group-orange?style=flat&logo=telegram",alt:"Join the chat at https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ"})})," \xa0",(0,i.jsx)(t.a,{href:"https://discord.gg/tYgADKx",children:(0,i.jsx)(t.img,{src:"https://img.shields.io/discord/719186998686122046?style=flat&label=Discord&logo=discord",alt:"Join the chat at https://discord.gg/tYgADKx"})})]}),"\n",(0,i.jsx)(t.p,{children:"See you there!"})]})}function h(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},88687:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/scheme_sketch-74c962b2089dc49399e093b1e9812403.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>s});var i=n(67294);const o={},a=i.createContext(o);function s(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1152],{9838:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});var i=n(85893),o=n(11151);const a={id:"introduction",title:"Centrifugo introduction"},s=void 0,r={id:"getting-started/introduction",title:"Centrifugo introduction",description:"Centrifugo is an open-source scalable real-time messaging server in a language-agnostic way.",source:"@site/versioned_docs/version-3/getting-started/introduction.md",sourceDirName:"getting-started",slug:"/getting-started/introduction",permalink:"/docs/3/getting-started/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/introduction.md",tags:[],version:"3",frontMatter:{id:"introduction",title:"Centrifugo introduction"},sidebar:"Introduction",next:{title:"Install Centrifugo",permalink:"/docs/3/getting-started/installation"}},c={},l=[{value:"Motivation",id:"motivation",level:2},{value:"Concepts",id:"concepts",level:2},{value:"Join community",id:"join-community",level:2}];function d(e){const t={a:"a",admonition:"admonition",h2:"h2",img:"img",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"Centrifugo is an open-source scalable real-time messaging server in a language-agnostic way."}),"\n",(0,i.jsx)(t.admonition,{title:"Real-time?",type:"info",children:(0,i.jsx)(t.p,{children:"By real-time, we indicate a soft real-time. Due to network latencies, garbage collection cycles, and so on, the delay of a delivered message can be up to several hundred milliseconds or higher."})}),"\n",(0,i.jsx)(t.p,{children:"It can be a missing piece in your application architecture to send real-time updates to users. Think chats, live comments, multiplayer games, streaming metrics \u2013 you'll be able to build amazing web and mobile real-time apps with a help of Centrifugo."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo works in conjunction with applications written in any programming language \u2013 both on the backend and frontend sides. It runs as a standalone service hosted on your hardware and fits well to both monolithic and microservice architectures."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is fast and scales well to support millions of concurrent client connections. It provides several real-time transports to choose from and a set of features to simplify building real-time applications."}),"\n",(0,i.jsx)(t.h2,{id:"motivation",children:"Motivation"}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo was born to help applications with a server-side written in a language or a framework without built-in concurrency support. In this case, dealing with persistent connections is a real headache that usually can only be resolved by introducing a shift in the technology stack and spending enough time to create a production-ready solution."}),"\n",(0,i.jsx)(t.p,{children:"For example, frameworks like Django, Flask, Yii, Laravel, Ruby on Rails, and others have poor and not performant support of working with many persistent connections for the real-time messaging task."}),"\n",(0,i.jsx)(t.p,{children:"In this case, Centrifugo is a very straightforward and non-obtrusive way to introduce real-time updates and handle lots of persistent connections without radical changes in application backend architecture. Developers could proceed writing a backend with a favorite language or favorite framework, keep existing architecture \u2013 and just let Centrifugo deal with persistent connections."}),"\n",(0,i.jsx)(t.p,{children:"At the moment, Centrifugo provides some advanced and unique features that can simplify a developer's life and save months of development, even if the application backend is built with the asynchronous concurrent language. One example is that Centrifugo can scale out-of-the-box to many machines with several supported brokers. And there are more things to mention \u2013 see detailed highlights further in the docs."}),"\n",(0,i.jsx)(t.h2,{id:"concepts",children:"Concepts"}),"\n",(0,i.jsx)(t.p,{children:"As mentioned above, Centrifugo runs as a standalone service that cares about handling persistent connections from application users. Application backend and frontend can be written in any programming language. Clients connect to Centrifugo and subscribe to channels."}),"\n",(0,i.jsx)(t.p,{children:"As soon as some event happens application backend can publish a message with event payload into a channel using Centrifugo API. The message will be delivered to all clients currently connected and subscribed to a channel."}),"\n",(0,i.jsx)(t.p,{children:"So Centrifugo is a user-facing PUB/SUB server in a nutshell. Here is a simplified scheme:"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Centrifugo scheme",src:n(15501).Z+"",width:"1186",height:"626"})}),"\n",(0,i.jsx)(t.h2,{id:"join-community",children:"Join community"}),"\n",(0,i.jsx)(t.p,{children:"We have rooms in Telegram and Discord:"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.a,{href:"https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ",children:(0,i.jsx)(t.img,{src:"https://img.shields.io/badge/Telegram-Group-orange?style=flat&logo=telegram",alt:"Join the chat at https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ"})})," \xa0",(0,i.jsx)(t.a,{href:"https://discord.gg/tYgADKx",children:(0,i.jsx)(t.img,{src:"https://img.shields.io/discord/719186998686122046?style=flat&label=Discord&logo=discord",alt:"Join the chat at https://discord.gg/tYgADKx"})})]}),"\n",(0,i.jsx)(t.p,{children:"See you there!"})]})}function h(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},15501:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/scheme_sketch-74c962b2089dc49399e093b1e9812403.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>s});var i=n(67294);const o={},a=i.createContext(o);function s(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/01a85c17.0c8a856b.js b/assets/js/01a85c17.0c8a856b.js deleted file mode 100644 index 4c09f0129..000000000 --- a/assets/js/01a85c17.0c8a856b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4013],{38762:(e,t,s)=>{s.d(t,{Z:()=>v});var a=s(67294),i=s(36905),r=s(78299),l=s(94980),n=s(75013),c=s(11614),o=s(16550),d=s(18407);function m(e){const{pathname:t}=(0,o.TH)();return(0,a.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,d.Mg)(e.permalink,t))}(e,t)))),[e,t])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var g=s(85893);function b(e){let{sidebar:t}=e;const s=m(t.items);return(0,g.jsx)("aside",{className:"col col--3",children:(0,g.jsxs)("nav",{className:(0,i.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,c.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,g.jsx)("div",{className:(0,i.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,g.jsx)("ul",{className:(0,i.Z)(u.sidebarItemList,"clean-list"),children:s.map((e=>(0,g.jsx)("li",{className:u.sidebarItem,children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var h=s(82306);function j(e){let{sidebar:t}=e;const s=m(t.items);return(0,g.jsx)("ul",{className:"menu__list",children:s.map((e=>(0,g.jsx)("li",{className:"menu__list-item",children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function p(e){return(0,g.jsx)(h.Zo,{component:j,props:e})}function x(e){let{sidebar:t}=e;const s=(0,l.i)();return t?.items.length?"mobile"===s?(0,g.jsx)(p,{sidebar:t}):(0,g.jsx)(b,{sidebar:t}):null}function v(e){const{sidebar:t,toc:s,children:a,...l}=e,n=t&&t.items.length>0;return(0,g.jsx)(r.Z,{...l,children:(0,g.jsx)("div",{className:"container margin-vert--lg",children:(0,g.jsxs)("div",{className:"row",children:[(0,g.jsx)(x,{sidebar:t}),(0,g.jsx)("main",{className:(0,i.Z)("col",{"col--7":n,"col--9 col--offset-1":!n}),children:a}),s&&(0,g.jsx)("div",{className:"col col--2",children:s})]})})})}},54057:(e,t,s)=>{s.r(t),s.d(t,{default:()=>j});s(67294);var a=s(36905),i=s(11614);const r=()=>(0,i.I)({id:"theme.tags.tagsPageTitle",message:"Tags",description:"The title of the tag list page"});var l=s(62581),n=s(18015),c=s(38762),o=s(24588),d=s(34055);const m={tag:"tag_Nnez"};var u=s(85893);function g(e){let{letterEntry:t}=e;return(0,u.jsxs)("article",{children:[(0,u.jsx)(d.Z,{as:"h2",id:t.letter,children:t.letter}),(0,u.jsx)("ul",{className:"padding--none",children:t.tags.map((e=>(0,u.jsx)("li",{className:m.tag,children:(0,u.jsx)(o.Z,{...e})},e.permalink)))}),(0,u.jsx)("hr",{})]})}function b(e){let{tags:t}=e;const s=function(e){const t={};return Object.values(e).forEach((e=>{const s=function(e){return e[0].toUpperCase()}(e.label);t[s]??=[],t[s].push(e)})),Object.entries(t).sort(((e,t)=>{let[s]=e,[a]=t;return s.localeCompare(a)})).map((e=>{let[t,s]=e;return{letter:t,tags:s.sort(((e,t)=>e.label.localeCompare(t.label)))}}))}(t);return(0,u.jsx)("section",{className:"margin-vert--lg",children:s.map((e=>(0,u.jsx)(g,{letterEntry:e},e.letter)))})}var h=s(26145);function j(e){let{tags:t,sidebar:s}=e;const i=r();return(0,u.jsxs)(l.FG,{className:(0,a.Z)(n.k.wrapper.blogPages,n.k.page.blogTagsListPage),children:[(0,u.jsx)(l.d,{title:i}),(0,u.jsx)(h.Z,{tag:"blog_tags_list"}),(0,u.jsxs)(c.Z,{sidebar:s,children:[(0,u.jsx)(d.Z,{as:"h1",children:i}),(0,u.jsx)(b,{tags:t})]})]})}},24588:(e,t,s)=>{s.d(t,{Z:()=>n});s(67294);var a=s(36905),i=s(75013);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=s(85893);function n(e){let{permalink:t,label:s,count:n}=e;return(0,l.jsxs)(i.Z,{href:t,className:(0,a.Z)(r.tag,n?r.tagWithCount:r.tagRegular),children:[s,n&&(0,l.jsx)("span",{children:n})]})}}}]); \ No newline at end of file diff --git a/assets/js/01a85c17.2f7dfa16.js b/assets/js/01a85c17.2f7dfa16.js new file mode 100644 index 000000000..b5631d061 --- /dev/null +++ b/assets/js/01a85c17.2f7dfa16.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4013],{61460:(e,t,s)=>{s.d(t,{Z:()=>v});var a=s(67294),i=s(36905),r=s(7372),l=s(87524),n=s(33692),c=s(95999),o=s(16550),d=s(48596);function m(e){const{pathname:t}=(0,o.TH)();return(0,a.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,d.Mg)(e.permalink,t))}(e,t)))),[e,t])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var g=s(85893);function b(e){let{sidebar:t}=e;const s=m(t.items);return(0,g.jsx)("aside",{className:"col col--3",children:(0,g.jsxs)("nav",{className:(0,i.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,c.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,g.jsx)("div",{className:(0,i.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,g.jsx)("ul",{className:(0,i.Z)(u.sidebarItemList,"clean-list"),children:s.map((e=>(0,g.jsx)("li",{className:u.sidebarItem,children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var h=s(13102);function j(e){let{sidebar:t}=e;const s=m(t.items);return(0,g.jsx)("ul",{className:"menu__list",children:s.map((e=>(0,g.jsx)("li",{className:"menu__list-item",children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function p(e){return(0,g.jsx)(h.Zo,{component:j,props:e})}function x(e){let{sidebar:t}=e;const s=(0,l.i)();return t?.items.length?"mobile"===s?(0,g.jsx)(p,{sidebar:t}):(0,g.jsx)(b,{sidebar:t}):null}function v(e){const{sidebar:t,toc:s,children:a,...l}=e,n=t&&t.items.length>0;return(0,g.jsx)(r.Z,{...l,children:(0,g.jsx)("div",{className:"container margin-vert--lg",children:(0,g.jsxs)("div",{className:"row",children:[(0,g.jsx)(x,{sidebar:t}),(0,g.jsx)("main",{className:(0,i.Z)("col",{"col--7":n,"col--9 col--offset-1":!n}),children:a}),s&&(0,g.jsx)("div",{className:"col col--2",children:s})]})})})}},91223:(e,t,s)=>{s.r(t),s.d(t,{default:()=>j});s(67294);var a=s(36905),i=s(95999);const r=()=>(0,i.I)({id:"theme.tags.tagsPageTitle",message:"Tags",description:"The title of the tag list page"});var l=s(71667),n=s(35281),c=s(61460),o=s(13008),d=s(92503);const m={tag:"tag_Nnez"};var u=s(85893);function g(e){let{letterEntry:t}=e;return(0,u.jsxs)("article",{children:[(0,u.jsx)(d.Z,{as:"h2",id:t.letter,children:t.letter}),(0,u.jsx)("ul",{className:"padding--none",children:t.tags.map((e=>(0,u.jsx)("li",{className:m.tag,children:(0,u.jsx)(o.Z,{...e})},e.permalink)))}),(0,u.jsx)("hr",{})]})}function b(e){let{tags:t}=e;const s=function(e){const t={};return Object.values(e).forEach((e=>{const s=function(e){return e[0].toUpperCase()}(e.label);t[s]??=[],t[s].push(e)})),Object.entries(t).sort(((e,t)=>{let[s]=e,[a]=t;return s.localeCompare(a)})).map((e=>{let[t,s]=e;return{letter:t,tags:s.sort(((e,t)=>e.label.localeCompare(t.label)))}}))}(t);return(0,u.jsx)("section",{className:"margin-vert--lg",children:s.map((e=>(0,u.jsx)(g,{letterEntry:e},e.letter)))})}var h=s(90197);function j(e){let{tags:t,sidebar:s}=e;const i=r();return(0,u.jsxs)(l.FG,{className:(0,a.Z)(n.k.wrapper.blogPages,n.k.page.blogTagsListPage),children:[(0,u.jsx)(l.d,{title:i}),(0,u.jsx)(h.Z,{tag:"blog_tags_list"}),(0,u.jsxs)(c.Z,{sidebar:s,children:[(0,u.jsx)(d.Z,{as:"h1",children:i}),(0,u.jsx)(b,{tags:t})]})]})}},13008:(e,t,s)=>{s.d(t,{Z:()=>n});s(67294);var a=s(36905),i=s(33692);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=s(85893);function n(e){let{permalink:t,label:s,count:n}=e;return(0,l.jsxs)(i.Z,{href:t,className:(0,a.Z)(r.tag,n?r.tagWithCount:r.tagRegular),children:[s,n&&(0,l.jsx)("span",{children:n})]})}}}]); \ No newline at end of file diff --git a/assets/js/01eca2db.56b499f1.js b/assets/js/01eca2db.000207f1.js similarity index 98% rename from assets/js/01eca2db.56b499f1.js rename to assets/js/01eca2db.000207f1.js index edadd23e2..e73e14366 100644 --- a/assets/js/01eca2db.56b499f1.js +++ b/assets/js/01eca2db.000207f1.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7717],{46499:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var i=n(85893),r=n(11151);const s={title:"Building a multi-room chat application with Laravel and Centrifugo",tags:["centrifugo","tutorial","laravel","php"],description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",author:"Anton Silischev",authorTitle:"Centrifugo contributor",authorImageURL:"https://github.com/silischev.png",image:"/img/laravel_centrifugo.jpg",hide_table_of_contents:!1},a=void 0,o={permalink:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2021-12-14-laravel-multi-room-chat-tutorial.md",source:"@site/blog/2021-12-14-laravel-multi-room-chat-tutorial.md",title:"Building a multi-room chat application with Laravel and Centrifugo",description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",date:"2021-12-14T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"tutorial",permalink:"/blog/tags/tutorial"},{label:"laravel",permalink:"/blog/tags/laravel"},{label:"php",permalink:"/blog/tags/php"}],readingTime:10.75,hasTruncateMarker:!0,authors:[{name:"Anton Silischev",title:"Centrifugo contributor",imageURL:"https://github.com/silischev.png"}],frontMatter:{title:"Building a multi-room chat application with Laravel and Centrifugo",tags:["centrifugo","tutorial","laravel","php"],description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",author:"Anton Silischev",authorTitle:"Centrifugo contributor",authorImageURL:"https://github.com/silischev.png",image:"/img/laravel_centrifugo.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Centrifugo v4 released \u2013 a little revolution",permalink:"/blog/2022/07/19/centrifugo-v4-released"},nextItem:{title:"Centrifugo integration with Django \u2013 building a basic chat application",permalink:"/blog/2021/11/04/integrating-with-django-building-chat-application"}},l={authorsImageUrls:[void 0]},c=[{value:"Application overview",id:"application-overview",level:2},{value:"Why integrate Laravel with Centrifugo?",id:"why-integrate-laravel-with-centrifugo",level:2},{value:"Setup and start a project",id:"setup-and-start-a-project",level:2},{value:"Application structure",id:"application-structure",level:2},{value:"Environment settings",id:"environment-settings",level:3},{value:"Database migrations and models",id:"database-migrations-and-models",level:3},{value:"Broadcasting",id:"broadcasting",level:3},{value:"Interaction with Centrifugo",id:"interaction-with-centrifugo",level:3},{value:"Connect proxy controller",id:"connect-proxy-controller",level:3},{value:"Room controller",id:"room-controller",level:3},{value:"Client side",id:"client-side",level:3},{value:"Possible improvements",id:"possible-improvements",level:2},{value:"Conclusion",id:"conclusion",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Image",src:n(48873).Z+"",width:"1500",height:"500"})}),"\n",(0,i.jsxs)(t.p,{children:["In this tutorial, we will create a multi-room chat server using ",(0,i.jsx)(t.a,{href:"https://laravel.com/",children:"Laravel framework"})," and ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/",children:"Centrifugo"})," real-time messaging server."]}),"\n",(0,i.jsx)(t.p,{children:"Authenticated users of our chat app will be able to create new chat rooms, join existing rooms and instantly communicate inside rooms with the help of Centrifugo WebSocket real-time transport."}),"\n",(0,i.jsx)(t.admonition,{type:"caution",children:(0,i.jsxs)(t.p,{children:["This tutorial was written for Centrifugo v3. We recently released ",(0,i.jsx)(t.a,{href:"/blog/2022/07/19/centrifugo-v4-released",children:"Centrifugo v4"})," which makes some parts of this tutorial obsolete. The core concepts are similar though \u2013 so this can still be used as a Centrifugo learning step."]})}),"\n",(0,i.jsx)(t.h2,{id:"application-overview",children:"Application overview"}),"\n",(0,i.jsx)(t.p,{children:"The result will look like this:"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/laravel_chat_demo.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.p,{children:"For the backend, we are using Laravel (version 8.65) as one of the most popular PHP frameworks. Centrifugo v3 will accept WebSocket client connections. And we will implement an integration layer between Laravel and Centrifugo."}),"\n",(0,i.jsx)(t.p,{children:"For CSS styles we are using recently released Bootstrap 5. Also, some vanilla JS instead of frameworks like React/Vue/whatever to make frontend Javascript code simple \u2013 so most developers out there could understand the mechanics."}),"\n",(0,i.jsx)(t.p,{children:"We are also using a bit old-fashioned server rendering here where server renders templates for different room routes (URLs) \u2013 i.e. our app is not a SPA app \u2013 mostly for the same reasons: to keep example short and let reader focus on Centrifugo and Laravel integration parts."}),"\n",(0,i.jsxs)(t.p,{children:["To generate fake user avatars we are requesting images from ",(0,i.jsx)(t.a,{href:"https://robohash.org/",children:"https://robohash.org/"})," which can generate unique robot puctures based on some input string (username in our case). Robots like to chat with each other!"]}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/1.png",width:"30%"}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/2.png",width:"30%"}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/4.png",width:"30%"}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsx)(t.p,{children:"We also have some ideas on further possible app improvements at the end of this post."})}),"\n",(0,i.jsx)(t.h2,{id:"why-integrate-laravel-with-centrifugo",children:"Why integrate Laravel with Centrifugo?"}),"\n",(0,i.jsx)(t.p,{children:"Why would Laravel developers want to integrate a project with Centrifugo for real-time messaging functionality? That's a good question. There are several points which could be a good motivation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Centrifugo is ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/centrifugo",children:"open-source"})," and ",(0,i.jsx)(t.strong,{children:"self-hosted"}),". So you can run it on your own infrastructure. Popular Laravel real-time broadcasting intergrations (Pusher and Ably) are paid cloud solutions. At scale Centrifugo will cost you less than cloud solutions. Of course cloud solutions do not require additional server setup \u2013 but everything is a trade-off right? So you should decide for youself."]}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo is fast and scales well. It has an optimized Redis Engine with client-side sharding and Redis Cluster support. Centrifugo can also scale with KeyDB, Nats, or Tarantool. So it's possible to handle millions of connections distributed over different Centrifugo nodes."}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo provides a variety of features out-of-the-box \u2013 some of them are unique, especially for self-hosted real-time servers that scale to many nodes (like fast message history cache, or maintaining single user connection, both client-side and server-side subscriptions, etc)."}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo is lightweight, single binary server which works as a separate service \u2013 it can be a universal tool in the developer's pocket, can migrate with you from one project to another, no matter what programming language or framework is used for business logic."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Hope this makes sense as a good motivation to give Centrifugo a try in your Laravel project. Let's get started!"}),"\n",(0,i.jsx)(t.h2,{id:"setup-and-start-a-project",children:"Setup and start a project"}),"\n",(0,i.jsxs)(t.p,{children:["For the convenience of working with the example, we ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker-compose.yml",children:"wrapped the end result into docker compose"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["To start the app clone ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples",children:"examples repo"}),", cd into ",(0,i.jsx)(t.code,{children:"v3/php_laravel_chat_tutorial"})," directory and run:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:"docker compose up\n"})}),"\n",(0,i.jsx)(t.p,{children:"At the first launch, the necessary images will be downloaded (will take some time and network bytes). When the main service is started, you should see something like this in container logs:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"...\napp | Database seeding completed successfully.\napp | [10-Dec-2021 12:25:05] NOTICE: fpm is running, pid 112\napp | [10-Dec-2021 12:25:05] NOTICE: ready to handle connections\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Then go to ",(0,i.jsx)(t.a,{href:"http://localhost/",children:"http://localhost/"})," \u2013 you should see:"]}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Image",src:n(12709).Z+"",width:"884",height:"453"})}),"\n",(0,i.jsx)(t.p,{children:"Register (using some fake credentials) or sign up \u2013 and proceed to the chat rooms."}),"\n",(0,i.jsxs)(t.p,{children:["Pay attention to the ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/docker/conf",children:"configuration"})," of Centrifugo and Nginx. Also, on ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker/entrypoints/app.sh",children:"entrypoint"})," which does some things:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"dependencies are installed via composer"}),"\n",(0,i.jsx)(t.li,{children:"copying settings from .env.example"}),"\n",(0,i.jsx)(t.li,{children:"db migrations are performed and the necessary npm packages are installed"}),"\n",(0,i.jsx)(t.li,{children:"php-fpm starts"}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"application-structure",children:"Application structure"}),"\n",(0,i.jsx)(t.p,{children:"We assume you already familar with Laravel concepts, so we will just point you to some core aspects of the Laravel application structure and will pay more attention to Centrifugo integration parts."}),"\n",(0,i.jsx)(t.h3,{id:"environment-settings",children:"Environment settings"}),"\n",(0,i.jsxs)(t.p,{children:["After the first launch of the application, all settings will be copied from the file ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/.env.example",children:(0,i.jsx)(t.code,{children:".env.example"})})," to ",(0,i.jsx)(t.code,{children:".env"}),". Next, we will take a closer look at some settings."]}),"\n",(0,i.jsx)(t.h3,{id:"database-migrations-and-models",children:"Database migrations and models"}),"\n",(0,i.jsxs)(t.p,{children:["You can view the database structure ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/app/database/migrations",children:"here"}),"."]}),"\n",(0,i.jsx)(t.p,{children:"We will use the following tables which will be then translated to the application models:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Laravel standard user authentication tables. See ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/authentication",children:"https://laravel.com/docs/8.x/authentication"}),". In the service we are using Laravel Breeze. For more information ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/starter-kits#laravel-breeze",children:"see official docs"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000001_create_rooms_table.php",children:"rooms"})," table. Basically - describes different rooms in the app every user can create."]}),"\n",(0,i.jsxs)(t.li,{children:["rooms ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000002_create_users_rooms_table.php",children:"many-to-many relation"})," to users. Allows to add users into rooms when ",(0,i.jsx)(t.code,{children:"join"})," button clicked or automatically upon room creation."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000003_create_messages_table.php",children:"messages"}),". Keeps message history in rooms."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"broadcasting",children:"Broadcasting"}),"\n",(0,i.jsxs)(t.p,{children:["For broadcasting we are using ",(0,i.jsx)(t.a,{href:"https://github.com/denis660/laravel-centrifugo",children:"laravel-centrifugo"})," library. It helps to simplify interaction between Laravel and Centrifugo by providing some convenient wrappers."]}),"\n",(0,i.jsxs)(t.p,{children:["Step-by-step configuration can be viewed in the ",(0,i.jsx)(t.a,{href:"https://github.com/denis660/laravel-centrifugo",children:"readme"})," file of this library."]}),"\n",(0,i.jsxs)(t.p,{children:["Pay attention to the ",(0,i.jsx)(t.code,{children:"CENTRIFUGO_API_KEY"})," setting. It is used to send API requests from Laravel to Centrifugo and must match in ",(0,i.jsx)(t.code,{children:".env"})," and ",(0,i.jsx)(t.code,{children:"centrifugo.json"})," files. And we also telling ",(0,i.jsx)(t.code,{children:"laravel-centrifugo"})," the URL of Centrifugo. That's all we need to configure for this example app."]}),"\n",(0,i.jsxs)(t.p,{children:["See more information about Laravel broadcasting ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/broadcasting",children:"here"}),"."]}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["As an alternative to ",(0,i.jsx)(t.code,{children:"laravel-centrifugo"}),", you can use ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/phpcent",children:"phpcent"})," \u2013 it's an official generic API client which allows publishing to Centrifugo HTTP API. But it does know nothing about Laravel broadcasting specifics."]})}),"\n",(0,i.jsx)(t.h3,{id:"interaction-with-centrifugo",children:"Interaction with Centrifugo"}),"\n",(0,i.jsx)(t.p,{children:"When user opens a chat app it connects to Centrifugo over WebSocket transport."}),"\n",(0,i.jsx)(t.p,{children:"Let's take a closer look at Centrifugo server configuration file we use for this example app:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-json",children:'{\n "port": 8000,\n "engine": "memory",\n "api_key": "some-long-api-key-which-you-should-keep-secret",\n "allowed_origins": [\n "http://localhost",\n ],\n "proxy_connect_endpoint": "http://nginx/centrifugo/connect/",\n "proxy_http_headers": [\n "Cookie"\n ],\n "namespaces": [\n {\n "name": "personal"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(t.p,{children:["This configuration defines a connect proxy endpoint which is targeting Nginx and then proxied to Laravel. Centrifugo will proxy ",(0,i.jsx)(t.code,{children:"Cookie"})," header of WebSocket HTTP Upgrade requests to Laravel \u2013 this allows using native Laravel authentication."]}),"\n",(0,i.jsxs)(t.p,{children:["We also defined a ",(0,i.jsx)(t.code,{children:'"personal"'})," namespace \u2013 we will subscribe each user to a personal channel in this namespace inside connect proxy handler. Using namespaces for different real-time features is one of Centrifugo best-practices."]}),"\n",(0,i.jsxs)(t.p,{children:["Allowed origins must be properly set to prevent ",(0,i.jsx)(t.a,{href:"https://christian-schneider.net/CrossSiteWebSocketHijacking.html",children:"cross-site WebSocket connection hijacking"}),"."]}),"\n",(0,i.jsx)(t.h3,{id:"connect-proxy-controller",children:"Connect proxy controller"}),"\n",(0,i.jsxs)(t.p,{children:["To use native Laravel user authentication middlewares, we will use ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/proxy",children:"Centrifugo proxy feature"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["When user connects to Centrifugo it's connection attempt will be transformed into HTTP request from Centrifugo to Laravel and will hit the ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/CentrifugoProxyController.php",children:"connect proxy controller"}),":"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-php",children:"class CentrifugoProxyController extends Controller\n{\n public function connect()\n {\n return new JsonResponse([\n 'result' => [\n 'user' => (string) Auth::user()->id,\n 'channels' => [\"personal:#\".Auth::user()->id],\n ]\n ]);\n }\n}\n"})}),"\n",(0,i.jsxs)(t.p,{children:["This controller ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/routes/api.php",children:"protected by auth middleware"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Since Centrifugo proxies ",(0,i.jsx)(t.code,{children:"Cookie"})," header of initial WebSocket HTTP Upgrade request Laravel auth layer will work just fine. So in a controller you already has access to the current authenticated user."]}),"\n",(0,i.jsxs)(t.p,{children:["In the response from controller we tell Centrifugo the ID of connecting user and subscribe user to its personal channel (using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/channels#user-channel-boundary-",children:"user-limited channel"})," feature of Centrifugo). Returning a channel in such way will subscribe user to it using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/server_subs",children:"server-side subscriptions"})," mechanism."]}),"\n",(0,i.jsxs)(t.admonition,{type:"tip",children:[(0,i.jsx)(t.p,{children:"Note, that in our chat app we are using a single personal channel for each user to receive real-time updates from all rooms. We are not creating separate subscriptions for each room user joined too. This will allow us to scale more easily in the future, and basically the only viable solution in case of room list pagination in chat application like this. It does not mean you can not combine personal user channels and separate room channels for different tasks though."}),(0,i.jsxs)(t.p,{children:["Some additional tips can be found in ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/faq/index#what-about-best-practices-with-the-number-of-channels",children:"Centrifugo FAQ"}),"."]})]}),"\n",(0,i.jsx)(t.h3,{id:"room-controller",children:"Room controller"}),"\n",(0,i.jsxs)(t.p,{children:["In ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/RoomController.php",children:"RoomController"})," we perform various actions with rooms:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"displaying rooms"}),"\n",(0,i.jsx)(t.li,{children:"create rooms"}),"\n",(0,i.jsx)(t.li,{children:"join users to rooms"}),"\n",(0,i.jsx)(t.li,{children:"publish messages"}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["When we publish a message in a room, we send a message to the personal channel of all users joined to the room using the ",(0,i.jsxs)(t.a,{href:"https://centrifugal.dev/docs/server/server_api#broadcast",children:[(0,i.jsx)(t.code,{children:"broadcast"})," method of Centrifugo API"]}),". It allows publishing the same message into many channels."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-php",children:'$message = Message::create([\n \'sender_id\' => Auth::user()->id,\n \'message\' => $requestData["message"],\n \'room_id\' => $id,\n]);\n\n$room = Room::with(\'users\')->find($id);\n\n$channels = [];\nforeach ($room->users as $user) {\n $channels[] = "personal:#" . $user->id;\n}\n\n$this->centrifugo->broadcast($channels, [\n "text" => $message->message,\n "createdAt" => $message->created_at->toDateTimeString(),\n "roomId" => $id,\n "senderId" => Auth::user()->id,\n "senderName" => Auth::user()->name,\n]);\n'})}),"\n",(0,i.jsxs)(t.p,{children:["We also add some fields to the published message which will be used when dynamically displaying a message coming from a WebSocket connection (see ",(0,i.jsx)(t.a,{href:"#client-side",children:"Client side"})," below)."]}),"\n",(0,i.jsx)(t.h3,{id:"client-side",children:"Client side"}),"\n",(0,i.jsxs)(t.p,{children:["Our chat is basically a one page with some variations dependng on the current route. So we use ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/resources/views/rooms/index.blade.php",children:"a single view"})," for the entire chat app."]}),"\n",(0,i.jsxs)(t.p,{children:["On the page we have a form for creating rooms. The user who created the room automatically joins it upon creation. Other users need to join manually (using ",(0,i.jsx)(t.code,{children:"join"})," button in the room)."]}),"\n",(0,i.jsxs)(t.p,{children:["When sending a message (using the chat room message input), we make an AJAX request that hits ",(0,i.jsx)(t.code,{children:"RoomController"})," shown above. A message saved into the database and then broadcasted to all users who joined this room. Here is a code that processes sending on ENTER:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:'messageInput.onkeyup = function(e) {\n if (e.keyCode === 13) {\n e.preventDefault();\n const message = messageInput.value;\n if (!message) {\n return;\n }\n const xhttp = new XMLHttpRequest();\n xhttp.open("POST", "/rooms/" + roomId + "/publish");\n xhttp.setRequestHeader("X-CSRF-TOKEN", csrfToken);\n xhttp.send(JSON.stringify({\n message: message\n }));\n messageInput.value = \'\';\n }\n};\n'})}),"\n",(0,i.jsxs)(t.p,{children:["After the message is processed on the server and broadcasted to Centrifugo it instantly comes to client-side. To receive the message we are connecting to Centrifugo WebSocket endpoint and wait for a message in the ",(0,i.jsx)(t.code,{children:"publish"})," event handler:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:'const url = "ws://" + window.location.host + "/connection/websocket";\nconst centrifuge = new Centrifuge(url);\n\ncentrifuge.on(\'connect\', function(ctx) {\n console.log("connected to Centrifugo", ctx);\n});\n\ncentrifuge.on(\'disconnect\', function(ctx) {\n console.log("disconnected from Centrifugo", ctx);\n});\n\ncentrifuge.on(\'publish\', function(ctx) {\n if (ctx.data.roomId.toString() === currentRoomId) {\n addMessage(ctx.data);\n scrollToLastMessage();\n }\n addRoomLastMessage(ctx.data);\n});\n\ncentrifuge.connect();\n'})}),"\n",(0,i.jsxs)(t.p,{children:["We are using ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/centrifuge-js",children:"centrifuge-js"})," client connector library to communicate with Centrifugo. This client abstracts away bidirectional asynchronous protocol complexity for us providing a simple way to listen connect, disconnect events and communicate with a server in various ways."]}),"\n",(0,i.jsx)(t.p,{children:"In publish event handler we check whether the message belongs to the room the user is currently in. If yes, then we add it to the message history of the room. We also add this message to the room in the list on the left as the last chat message in room. If necessary, we crop the text for normal display."}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["In our example we only subscribe each user to a single channel, but user can be subscribed to several server-side channels. To distinguish between them use ",(0,i.jsx)(t.code,{children:"ctx.channel"})," inside publish event handler."]})}),"\n",(0,i.jsx)(t.p,{children:"And that's it! We went through all the main parts of the integration."}),"\n",(0,i.jsx)(t.h2,{id:"possible-improvements",children:"Possible improvements"}),"\n",(0,i.jsx)(t.p,{children:"As promised, here is a list with several possible app improvements:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Transform to a single page app, use productive Javascript frameworks like React or VueJS instead of vanilla JS."}),"\n",(0,i.jsx)(t.li,{children:"Add message read statuses - as soon as one of the chat participants read the message mark it read in the database."}),"\n",(0,i.jsx)(t.li,{children:"Introduce user-to-user chats."}),"\n",(0,i.jsx)(t.li,{children:"Support pagination for the message history, maybe for chat room list also."}),"\n",(0,i.jsx)(t.li,{children:"Don't show all rooms in the system \u2013 add functionality to search room by name."}),"\n",(0,i.jsxs)(t.li,{children:["Horizontal scaling (using multiple nodes of Centrifugo, for example with ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/engines#redis-engine",children:"Redis Engine"}),") \u2013 mostly one line in Centrifugo config if you have Redis running."]}),"\n",(0,i.jsx)(t.li,{children:"Gracefully handle temporary disconnects by loading missed messages from the database or Centrifugo channel history cache."}),"\n",(0,i.jsxs)(t.li,{children:["Optionally replace connect proxy with ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/authentication",children:"JWT authentication"})," to reduce HTTP calls from Centrifugo to Laravel. This may drastically reduce resources for Laravel backend at scale."]}),"\n",(0,i.jsxs)(t.li,{children:["Try using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/proxy#rpc-proxy",children:"Centrifugo RPC proxy"})," feature to use WebSocket connection for message publish instead of issuing AJAX request."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(t.p,{children:"We built a chat app with Laravel and Centrifugo. While there is still an area for improvements, this example is not really the basic. It's already valuable in the current form and may be transformed into part of your production system with minimal tweaks."}),"\n",(0,i.jsxs)(t.p,{children:["Hope you enjoyed this tutorial. If you have any questions after reading \u2013 join our ",(0,i.jsx)(t.a,{href:"/docs/getting-started/introduction#join-community",children:"community channels"}),". We touched only part of Centrifugo concepts here \u2013 take a look at detailed Centrifugo docs nearby. And let the Centrifugal force be with you!"]})]})}function d(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},48873:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/laravel_centrifugo-0ccb001662ef66c6d19abca6208e8966.jpg"},12709:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/laravel_main_page-c3e70ae9857eefd3ca4fcd9999a7962c.jpg"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7717],{46499:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var i=n(85893),r=n(11151);const s={title:"Building a multi-room chat application with Laravel and Centrifugo",tags:["centrifugo","tutorial","laravel","php"],description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",author:"Anton Silischev",authorTitle:"Centrifugo contributor",authorImageURL:"https://github.com/silischev.png",image:"/img/laravel_centrifugo.jpg",hide_table_of_contents:!1},a=void 0,o={permalink:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2021-12-14-laravel-multi-room-chat-tutorial.md",source:"@site/blog/2021-12-14-laravel-multi-room-chat-tutorial.md",title:"Building a multi-room chat application with Laravel and Centrifugo",description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",date:"2021-12-14T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"tutorial",permalink:"/blog/tags/tutorial"},{label:"laravel",permalink:"/blog/tags/laravel"},{label:"php",permalink:"/blog/tags/php"}],readingTime:10.75,hasTruncateMarker:!0,authors:[{name:"Anton Silischev",title:"Centrifugo contributor",imageURL:"https://github.com/silischev.png"}],frontMatter:{title:"Building a multi-room chat application with Laravel and Centrifugo",tags:["centrifugo","tutorial","laravel","php"],description:"In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",author:"Anton Silischev",authorTitle:"Centrifugo contributor",authorImageURL:"https://github.com/silischev.png",image:"/img/laravel_centrifugo.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Centrifugo v4 released \u2013 a little revolution",permalink:"/blog/2022/07/19/centrifugo-v4-released"},nextItem:{title:"Centrifugo integration with Django \u2013 building a basic chat application",permalink:"/blog/2021/11/04/integrating-with-django-building-chat-application"}},l={authorsImageUrls:[void 0]},c=[{value:"Application overview",id:"application-overview",level:2},{value:"Why integrate Laravel with Centrifugo?",id:"why-integrate-laravel-with-centrifugo",level:2},{value:"Setup and start a project",id:"setup-and-start-a-project",level:2},{value:"Application structure",id:"application-structure",level:2},{value:"Environment settings",id:"environment-settings",level:3},{value:"Database migrations and models",id:"database-migrations-and-models",level:3},{value:"Broadcasting",id:"broadcasting",level:3},{value:"Interaction with Centrifugo",id:"interaction-with-centrifugo",level:3},{value:"Connect proxy controller",id:"connect-proxy-controller",level:3},{value:"Room controller",id:"room-controller",level:3},{value:"Client side",id:"client-side",level:3},{value:"Possible improvements",id:"possible-improvements",level:2},{value:"Conclusion",id:"conclusion",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Image",src:n(72717).Z+"",width:"1500",height:"500"})}),"\n",(0,i.jsxs)(t.p,{children:["In this tutorial, we will create a multi-room chat server using ",(0,i.jsx)(t.a,{href:"https://laravel.com/",children:"Laravel framework"})," and ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/",children:"Centrifugo"})," real-time messaging server."]}),"\n",(0,i.jsx)(t.p,{children:"Authenticated users of our chat app will be able to create new chat rooms, join existing rooms and instantly communicate inside rooms with the help of Centrifugo WebSocket real-time transport."}),"\n",(0,i.jsx)(t.admonition,{type:"caution",children:(0,i.jsxs)(t.p,{children:["This tutorial was written for Centrifugo v3. We recently released ",(0,i.jsx)(t.a,{href:"/blog/2022/07/19/centrifugo-v4-released",children:"Centrifugo v4"})," which makes some parts of this tutorial obsolete. The core concepts are similar though \u2013 so this can still be used as a Centrifugo learning step."]})}),"\n",(0,i.jsx)(t.h2,{id:"application-overview",children:"Application overview"}),"\n",(0,i.jsx)(t.p,{children:"The result will look like this:"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/laravel_chat_demo.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.p,{children:"For the backend, we are using Laravel (version 8.65) as one of the most popular PHP frameworks. Centrifugo v3 will accept WebSocket client connections. And we will implement an integration layer between Laravel and Centrifugo."}),"\n",(0,i.jsx)(t.p,{children:"For CSS styles we are using recently released Bootstrap 5. Also, some vanilla JS instead of frameworks like React/Vue/whatever to make frontend Javascript code simple \u2013 so most developers out there could understand the mechanics."}),"\n",(0,i.jsx)(t.p,{children:"We are also using a bit old-fashioned server rendering here where server renders templates for different room routes (URLs) \u2013 i.e. our app is not a SPA app \u2013 mostly for the same reasons: to keep example short and let reader focus on Centrifugo and Laravel integration parts."}),"\n",(0,i.jsxs)(t.p,{children:["To generate fake user avatars we are requesting images from ",(0,i.jsx)(t.a,{href:"https://robohash.org/",children:"https://robohash.org/"})," which can generate unique robot puctures based on some input string (username in our case). Robots like to chat with each other!"]}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/1.png",width:"30%"}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/2.png",width:"30%"}),"\n",(0,i.jsx)("img",{src:"https://robohash.org/4.png",width:"30%"}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)("br",{}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsx)(t.p,{children:"We also have some ideas on further possible app improvements at the end of this post."})}),"\n",(0,i.jsx)(t.h2,{id:"why-integrate-laravel-with-centrifugo",children:"Why integrate Laravel with Centrifugo?"}),"\n",(0,i.jsx)(t.p,{children:"Why would Laravel developers want to integrate a project with Centrifugo for real-time messaging functionality? That's a good question. There are several points which could be a good motivation:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Centrifugo is ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/centrifugo",children:"open-source"})," and ",(0,i.jsx)(t.strong,{children:"self-hosted"}),". So you can run it on your own infrastructure. Popular Laravel real-time broadcasting intergrations (Pusher and Ably) are paid cloud solutions. At scale Centrifugo will cost you less than cloud solutions. Of course cloud solutions do not require additional server setup \u2013 but everything is a trade-off right? So you should decide for youself."]}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo is fast and scales well. It has an optimized Redis Engine with client-side sharding and Redis Cluster support. Centrifugo can also scale with KeyDB, Nats, or Tarantool. So it's possible to handle millions of connections distributed over different Centrifugo nodes."}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo provides a variety of features out-of-the-box \u2013 some of them are unique, especially for self-hosted real-time servers that scale to many nodes (like fast message history cache, or maintaining single user connection, both client-side and server-side subscriptions, etc)."}),"\n",(0,i.jsx)(t.li,{children:"Centrifugo is lightweight, single binary server which works as a separate service \u2013 it can be a universal tool in the developer's pocket, can migrate with you from one project to another, no matter what programming language or framework is used for business logic."}),"\n"]}),"\n",(0,i.jsx)(t.p,{children:"Hope this makes sense as a good motivation to give Centrifugo a try in your Laravel project. Let's get started!"}),"\n",(0,i.jsx)(t.h2,{id:"setup-and-start-a-project",children:"Setup and start a project"}),"\n",(0,i.jsxs)(t.p,{children:["For the convenience of working with the example, we ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker-compose.yml",children:"wrapped the end result into docker compose"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["To start the app clone ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples",children:"examples repo"}),", cd into ",(0,i.jsx)(t.code,{children:"v3/php_laravel_chat_tutorial"})," directory and run:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:"docker compose up\n"})}),"\n",(0,i.jsx)(t.p,{children:"At the first launch, the necessary images will be downloaded (will take some time and network bytes). When the main service is started, you should see something like this in container logs:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"...\napp | Database seeding completed successfully.\napp | [10-Dec-2021 12:25:05] NOTICE: fpm is running, pid 112\napp | [10-Dec-2021 12:25:05] NOTICE: ready to handle connections\n"})}),"\n",(0,i.jsxs)(t.p,{children:["Then go to ",(0,i.jsx)(t.a,{href:"http://localhost/",children:"http://localhost/"})," \u2013 you should see:"]}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Image",src:n(77199).Z+"",width:"884",height:"453"})}),"\n",(0,i.jsx)(t.p,{children:"Register (using some fake credentials) or sign up \u2013 and proceed to the chat rooms."}),"\n",(0,i.jsxs)(t.p,{children:["Pay attention to the ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/docker/conf",children:"configuration"})," of Centrifugo and Nginx. Also, on ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker/entrypoints/app.sh",children:"entrypoint"})," which does some things:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"dependencies are installed via composer"}),"\n",(0,i.jsx)(t.li,{children:"copying settings from .env.example"}),"\n",(0,i.jsx)(t.li,{children:"db migrations are performed and the necessary npm packages are installed"}),"\n",(0,i.jsx)(t.li,{children:"php-fpm starts"}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"application-structure",children:"Application structure"}),"\n",(0,i.jsx)(t.p,{children:"We assume you already familar with Laravel concepts, so we will just point you to some core aspects of the Laravel application structure and will pay more attention to Centrifugo integration parts."}),"\n",(0,i.jsx)(t.h3,{id:"environment-settings",children:"Environment settings"}),"\n",(0,i.jsxs)(t.p,{children:["After the first launch of the application, all settings will be copied from the file ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/.env.example",children:(0,i.jsx)(t.code,{children:".env.example"})})," to ",(0,i.jsx)(t.code,{children:".env"}),". Next, we will take a closer look at some settings."]}),"\n",(0,i.jsx)(t.h3,{id:"database-migrations-and-models",children:"Database migrations and models"}),"\n",(0,i.jsxs)(t.p,{children:["You can view the database structure ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/app/database/migrations",children:"here"}),"."]}),"\n",(0,i.jsx)(t.p,{children:"We will use the following tables which will be then translated to the application models:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["Laravel standard user authentication tables. See ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/authentication",children:"https://laravel.com/docs/8.x/authentication"}),". In the service we are using Laravel Breeze. For more information ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/starter-kits#laravel-breeze",children:"see official docs"}),"."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000001_create_rooms_table.php",children:"rooms"})," table. Basically - describes different rooms in the app every user can create."]}),"\n",(0,i.jsxs)(t.li,{children:["rooms ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000002_create_users_rooms_table.php",children:"many-to-many relation"})," to users. Allows to add users into rooms when ",(0,i.jsx)(t.code,{children:"join"})," button clicked or automatically upon room creation."]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000003_create_messages_table.php",children:"messages"}),". Keeps message history in rooms."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"broadcasting",children:"Broadcasting"}),"\n",(0,i.jsxs)(t.p,{children:["For broadcasting we are using ",(0,i.jsx)(t.a,{href:"https://github.com/denis660/laravel-centrifugo",children:"laravel-centrifugo"})," library. It helps to simplify interaction between Laravel and Centrifugo by providing some convenient wrappers."]}),"\n",(0,i.jsxs)(t.p,{children:["Step-by-step configuration can be viewed in the ",(0,i.jsx)(t.a,{href:"https://github.com/denis660/laravel-centrifugo",children:"readme"})," file of this library."]}),"\n",(0,i.jsxs)(t.p,{children:["Pay attention to the ",(0,i.jsx)(t.code,{children:"CENTRIFUGO_API_KEY"})," setting. It is used to send API requests from Laravel to Centrifugo and must match in ",(0,i.jsx)(t.code,{children:".env"})," and ",(0,i.jsx)(t.code,{children:"centrifugo.json"})," files. And we also telling ",(0,i.jsx)(t.code,{children:"laravel-centrifugo"})," the URL of Centrifugo. That's all we need to configure for this example app."]}),"\n",(0,i.jsxs)(t.p,{children:["See more information about Laravel broadcasting ",(0,i.jsx)(t.a,{href:"https://laravel.com/docs/8.x/broadcasting",children:"here"}),"."]}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["As an alternative to ",(0,i.jsx)(t.code,{children:"laravel-centrifugo"}),", you can use ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/phpcent",children:"phpcent"})," \u2013 it's an official generic API client which allows publishing to Centrifugo HTTP API. But it does know nothing about Laravel broadcasting specifics."]})}),"\n",(0,i.jsx)(t.h3,{id:"interaction-with-centrifugo",children:"Interaction with Centrifugo"}),"\n",(0,i.jsx)(t.p,{children:"When user opens a chat app it connects to Centrifugo over WebSocket transport."}),"\n",(0,i.jsx)(t.p,{children:"Let's take a closer look at Centrifugo server configuration file we use for this example app:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-json",children:'{\n "port": 8000,\n "engine": "memory",\n "api_key": "some-long-api-key-which-you-should-keep-secret",\n "allowed_origins": [\n "http://localhost",\n ],\n "proxy_connect_endpoint": "http://nginx/centrifugo/connect/",\n "proxy_http_headers": [\n "Cookie"\n ],\n "namespaces": [\n {\n "name": "personal"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(t.p,{children:["This configuration defines a connect proxy endpoint which is targeting Nginx and then proxied to Laravel. Centrifugo will proxy ",(0,i.jsx)(t.code,{children:"Cookie"})," header of WebSocket HTTP Upgrade requests to Laravel \u2013 this allows using native Laravel authentication."]}),"\n",(0,i.jsxs)(t.p,{children:["We also defined a ",(0,i.jsx)(t.code,{children:'"personal"'})," namespace \u2013 we will subscribe each user to a personal channel in this namespace inside connect proxy handler. Using namespaces for different real-time features is one of Centrifugo best-practices."]}),"\n",(0,i.jsxs)(t.p,{children:["Allowed origins must be properly set to prevent ",(0,i.jsx)(t.a,{href:"https://christian-schneider.net/CrossSiteWebSocketHijacking.html",children:"cross-site WebSocket connection hijacking"}),"."]}),"\n",(0,i.jsx)(t.h3,{id:"connect-proxy-controller",children:"Connect proxy controller"}),"\n",(0,i.jsxs)(t.p,{children:["To use native Laravel user authentication middlewares, we will use ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/proxy",children:"Centrifugo proxy feature"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["When user connects to Centrifugo it's connection attempt will be transformed into HTTP request from Centrifugo to Laravel and will hit the ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/CentrifugoProxyController.php",children:"connect proxy controller"}),":"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-php",children:"class CentrifugoProxyController extends Controller\n{\n public function connect()\n {\n return new JsonResponse([\n 'result' => [\n 'user' => (string) Auth::user()->id,\n 'channels' => [\"personal:#\".Auth::user()->id],\n ]\n ]);\n }\n}\n"})}),"\n",(0,i.jsxs)(t.p,{children:["This controller ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/routes/api.php",children:"protected by auth middleware"}),"."]}),"\n",(0,i.jsxs)(t.p,{children:["Since Centrifugo proxies ",(0,i.jsx)(t.code,{children:"Cookie"})," header of initial WebSocket HTTP Upgrade request Laravel auth layer will work just fine. So in a controller you already has access to the current authenticated user."]}),"\n",(0,i.jsxs)(t.p,{children:["In the response from controller we tell Centrifugo the ID of connecting user and subscribe user to its personal channel (using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/channels#user-channel-boundary-",children:"user-limited channel"})," feature of Centrifugo). Returning a channel in such way will subscribe user to it using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/server_subs",children:"server-side subscriptions"})," mechanism."]}),"\n",(0,i.jsxs)(t.admonition,{type:"tip",children:[(0,i.jsx)(t.p,{children:"Note, that in our chat app we are using a single personal channel for each user to receive real-time updates from all rooms. We are not creating separate subscriptions for each room user joined too. This will allow us to scale more easily in the future, and basically the only viable solution in case of room list pagination in chat application like this. It does not mean you can not combine personal user channels and separate room channels for different tasks though."}),(0,i.jsxs)(t.p,{children:["Some additional tips can be found in ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/faq/index#what-about-best-practices-with-the-number-of-channels",children:"Centrifugo FAQ"}),"."]})]}),"\n",(0,i.jsx)(t.h3,{id:"room-controller",children:"Room controller"}),"\n",(0,i.jsxs)(t.p,{children:["In ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/RoomController.php",children:"RoomController"})," we perform various actions with rooms:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"displaying rooms"}),"\n",(0,i.jsx)(t.li,{children:"create rooms"}),"\n",(0,i.jsx)(t.li,{children:"join users to rooms"}),"\n",(0,i.jsx)(t.li,{children:"publish messages"}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["When we publish a message in a room, we send a message to the personal channel of all users joined to the room using the ",(0,i.jsxs)(t.a,{href:"https://centrifugal.dev/docs/server/server_api#broadcast",children:[(0,i.jsx)(t.code,{children:"broadcast"})," method of Centrifugo API"]}),". It allows publishing the same message into many channels."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-php",children:'$message = Message::create([\n \'sender_id\' => Auth::user()->id,\n \'message\' => $requestData["message"],\n \'room_id\' => $id,\n]);\n\n$room = Room::with(\'users\')->find($id);\n\n$channels = [];\nforeach ($room->users as $user) {\n $channels[] = "personal:#" . $user->id;\n}\n\n$this->centrifugo->broadcast($channels, [\n "text" => $message->message,\n "createdAt" => $message->created_at->toDateTimeString(),\n "roomId" => $id,\n "senderId" => Auth::user()->id,\n "senderName" => Auth::user()->name,\n]);\n'})}),"\n",(0,i.jsxs)(t.p,{children:["We also add some fields to the published message which will be used when dynamically displaying a message coming from a WebSocket connection (see ",(0,i.jsx)(t.a,{href:"#client-side",children:"Client side"})," below)."]}),"\n",(0,i.jsx)(t.h3,{id:"client-side",children:"Client side"}),"\n",(0,i.jsxs)(t.p,{children:["Our chat is basically a one page with some variations dependng on the current route. So we use ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/resources/views/rooms/index.blade.php",children:"a single view"})," for the entire chat app."]}),"\n",(0,i.jsxs)(t.p,{children:["On the page we have a form for creating rooms. The user who created the room automatically joins it upon creation. Other users need to join manually (using ",(0,i.jsx)(t.code,{children:"join"})," button in the room)."]}),"\n",(0,i.jsxs)(t.p,{children:["When sending a message (using the chat room message input), we make an AJAX request that hits ",(0,i.jsx)(t.code,{children:"RoomController"})," shown above. A message saved into the database and then broadcasted to all users who joined this room. Here is a code that processes sending on ENTER:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:'messageInput.onkeyup = function(e) {\n if (e.keyCode === 13) {\n e.preventDefault();\n const message = messageInput.value;\n if (!message) {\n return;\n }\n const xhttp = new XMLHttpRequest();\n xhttp.open("POST", "/rooms/" + roomId + "/publish");\n xhttp.setRequestHeader("X-CSRF-TOKEN", csrfToken);\n xhttp.send(JSON.stringify({\n message: message\n }));\n messageInput.value = \'\';\n }\n};\n'})}),"\n",(0,i.jsxs)(t.p,{children:["After the message is processed on the server and broadcasted to Centrifugo it instantly comes to client-side. To receive the message we are connecting to Centrifugo WebSocket endpoint and wait for a message in the ",(0,i.jsx)(t.code,{children:"publish"})," event handler:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-js",children:'const url = "ws://" + window.location.host + "/connection/websocket";\nconst centrifuge = new Centrifuge(url);\n\ncentrifuge.on(\'connect\', function(ctx) {\n console.log("connected to Centrifugo", ctx);\n});\n\ncentrifuge.on(\'disconnect\', function(ctx) {\n console.log("disconnected from Centrifugo", ctx);\n});\n\ncentrifuge.on(\'publish\', function(ctx) {\n if (ctx.data.roomId.toString() === currentRoomId) {\n addMessage(ctx.data);\n scrollToLastMessage();\n }\n addRoomLastMessage(ctx.data);\n});\n\ncentrifuge.connect();\n'})}),"\n",(0,i.jsxs)(t.p,{children:["We are using ",(0,i.jsx)(t.a,{href:"https://github.com/centrifugal/centrifuge-js",children:"centrifuge-js"})," client connector library to communicate with Centrifugo. This client abstracts away bidirectional asynchronous protocol complexity for us providing a simple way to listen connect, disconnect events and communicate with a server in various ways."]}),"\n",(0,i.jsx)(t.p,{children:"In publish event handler we check whether the message belongs to the room the user is currently in. If yes, then we add it to the message history of the room. We also add this message to the room in the list on the left as the last chat message in room. If necessary, we crop the text for normal display."}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["In our example we only subscribe each user to a single channel, but user can be subscribed to several server-side channels. To distinguish between them use ",(0,i.jsx)(t.code,{children:"ctx.channel"})," inside publish event handler."]})}),"\n",(0,i.jsx)(t.p,{children:"And that's it! We went through all the main parts of the integration."}),"\n",(0,i.jsx)(t.h2,{id:"possible-improvements",children:"Possible improvements"}),"\n",(0,i.jsx)(t.p,{children:"As promised, here is a list with several possible app improvements:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Transform to a single page app, use productive Javascript frameworks like React or VueJS instead of vanilla JS."}),"\n",(0,i.jsx)(t.li,{children:"Add message read statuses - as soon as one of the chat participants read the message mark it read in the database."}),"\n",(0,i.jsx)(t.li,{children:"Introduce user-to-user chats."}),"\n",(0,i.jsx)(t.li,{children:"Support pagination for the message history, maybe for chat room list also."}),"\n",(0,i.jsx)(t.li,{children:"Don't show all rooms in the system \u2013 add functionality to search room by name."}),"\n",(0,i.jsxs)(t.li,{children:["Horizontal scaling (using multiple nodes of Centrifugo, for example with ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/engines#redis-engine",children:"Redis Engine"}),") \u2013 mostly one line in Centrifugo config if you have Redis running."]}),"\n",(0,i.jsx)(t.li,{children:"Gracefully handle temporary disconnects by loading missed messages from the database or Centrifugo channel history cache."}),"\n",(0,i.jsxs)(t.li,{children:["Optionally replace connect proxy with ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/authentication",children:"JWT authentication"})," to reduce HTTP calls from Centrifugo to Laravel. This may drastically reduce resources for Laravel backend at scale."]}),"\n",(0,i.jsxs)(t.li,{children:["Try using ",(0,i.jsx)(t.a,{href:"https://centrifugal.dev/docs/server/proxy#rpc-proxy",children:"Centrifugo RPC proxy"})," feature to use WebSocket connection for message publish instead of issuing AJAX request."]}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(t.p,{children:"We built a chat app with Laravel and Centrifugo. While there is still an area for improvements, this example is not really the basic. It's already valuable in the current form and may be transformed into part of your production system with minimal tweaks."}),"\n",(0,i.jsxs)(t.p,{children:["Hope you enjoyed this tutorial. If you have any questions after reading \u2013 join our ",(0,i.jsx)(t.a,{href:"/docs/getting-started/introduction#join-community",children:"community channels"}),". We touched only part of Centrifugo concepts here \u2013 take a look at detailed Centrifugo docs nearby. And let the Centrifugal force be with you!"]})]})}function d(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},72717:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/laravel_centrifugo-0ccb001662ef66c6d19abca6208e8966.jpg"},77199:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/laravel_main_page-c3e70ae9857eefd3ca4fcd9999a7962c.jpg"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/04ac3258.dc3fa22f.js b/assets/js/04ac3258.d14e9138.js similarity index 96% rename from assets/js/04ac3258.dc3fa22f.js rename to assets/js/04ac3258.d14e9138.js index 6455262d8..204129bd8 100644 --- a/assets/js/04ac3258.dc3fa22f.js +++ b/assets/js/04ac3258.d14e9138.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8356],{39267:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=n(85893),r=n(11151);const s={id:"tracing",title:"User and channel tracing"},a=void 0,o={id:"pro/tracing",title:"User and channel tracing",description:"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.",source:"@site/docs/pro/tracing.md",sourceDirName:"pro",slug:"/pro/tracing",permalink:"/docs/pro/tracing",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/tracing.md",tags:[],version:"current",frontMatter:{id:"tracing",title:"User and channel tracing"},sidebar:"Pro",previous:{title:"Install and run PRO version",permalink:"/docs/pro/install_and_run"},next:{title:"Analytics with ClickHouse",permalink:"/docs/pro/analytics"}},c={},l=[{value:"Save to a file",id:"save-to-a-file",level:3}];function d(e){const t={code:"code",h3:"h3",img:"img",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"tracing",src:n(27298).Z+"",width:"3862",height:"925"})}),"\n",(0,i.jsx)(t.p,{children:"It's possible to attach to trace streams using Centrifugo admin UI panel or simply from terminal using CURL and admin token."}),"\n",(0,i.jsx)(t.p,{children:"This can be super-useful for debugging issues, investigating application behavior, understanding that the application works as expected."}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/tracing_ui.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.h3,{id:"save-to-a-file",children:"Save to a file"}),"\n",(0,i.jsx)(t.p,{children:"It's possible to connect to the admin tracing endpoint with CURL using the admin session token. And then save tracing output to a file for later processing."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:'curl -X POST http://localhost:8000/admin/trace -H "Authorization: token " -d \'{"type": "user", "entity": "56"}\' -o trace.txt\n'})}),"\n",(0,i.jsx)(t.p,{children:"Currently, you should copy the admin auth token from browser developer tools, this may be improved in the future as PRO version evolves."})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},27298:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/tracing-fd844bdc776dc14d4061afd620b7b70b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8356],{39267:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=n(85893),r=n(11151);const s={id:"tracing",title:"User and channel tracing"},a=void 0,o={id:"pro/tracing",title:"User and channel tracing",description:"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.",source:"@site/docs/pro/tracing.md",sourceDirName:"pro",slug:"/pro/tracing",permalink:"/docs/pro/tracing",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/tracing.md",tags:[],version:"current",frontMatter:{id:"tracing",title:"User and channel tracing"},sidebar:"Pro",previous:{title:"Install and run PRO version",permalink:"/docs/pro/install_and_run"},next:{title:"Analytics with ClickHouse",permalink:"/docs/pro/analytics"}},c={},l=[{value:"Save to a file",id:"save-to-a-file",level:3}];function d(e){const t={code:"code",h3:"h3",img:"img",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"tracing",src:n(64164).Z+"",width:"3862",height:"925"})}),"\n",(0,i.jsx)(t.p,{children:"It's possible to attach to trace streams using Centrifugo admin UI panel or simply from terminal using CURL and admin token."}),"\n",(0,i.jsx)(t.p,{children:"This can be super-useful for debugging issues, investigating application behavior, understanding that the application works as expected."}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/tracing_ui.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.h3,{id:"save-to-a-file",children:"Save to a file"}),"\n",(0,i.jsx)(t.p,{children:"It's possible to connect to the admin tracing endpoint with CURL using the admin session token. And then save tracing output to a file for later processing."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:'curl -X POST http://localhost:8000/admin/trace -H "Authorization: token " -d \'{"type": "user", "entity": "56"}\' -o trace.txt\n'})}),"\n",(0,i.jsx)(t.p,{children:"Currently, you should copy the admin auth token from browser developer tools, this may be improved in the future as PRO version evolves."})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},64164:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/tracing-fd844bdc776dc14d4061afd620b7b70b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/06f9ead7.6851e4ba.js b/assets/js/06f9ead7.c5420918.js similarity index 99% rename from assets/js/06f9ead7.6851e4ba.js rename to assets/js/06f9ead7.c5420918.js index 040c1eaf0..232718ced 100644 --- a/assets/js/06f9ead7.6851e4ba.js +++ b/assets/js/06f9ead7.c5420918.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8896],{31929:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>d,frontMatter:()=>r,metadata:()=>l,toc:()=>t});var i=s(85893),o=s(11151);const r={id:"channels",title:"Channels and namespaces"},a=void 0,l={id:"server/channels",title:"Channels and namespaces",description:"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.",source:"@site/docs/server/channels.md",sourceDirName:"server",slug:"/server/channels",permalink:"/docs/server/channels",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/channels.md",tags:[],version:"current",frontMatter:{id:"channels",title:"Channels and namespaces"},sidebar:"Guides",previous:{title:"Client JWT authentication",permalink:"/docs/server/authentication"},next:{title:"Channel permission model",permalink:"/docs/server/channel_permissions"}},c={},t=[{value:"What is a channel?",id:"what-is-a-channel",level:2},{value:"Channel name rules",id:"channel-name-rules",level:2},{value:"namespace boundary (:)",id:"namespace-boundary-",level:3},{value:"user channel boundary (#)",id:"user-channel-boundary-",level:3},{value:"private channel prefix ($)",id:"private-channel-prefix-",level:3},{value:"Channel is just a string",id:"channel-is-just-a-string",level:3},{value:"Channel namespaces",id:"channel-namespaces",level:2},{value:"Channel options",id:"channel-options",level:2},{value:"presence",id:"presence",level:3},{value:"join_leave",id:"join_leave",level:3},{value:"force_push_join_leave",id:"force_push_join_leave",level:3},{value:"history_size",id:"history_size",level:3},{value:"history_ttl",id:"history_ttl",level:3},{value:"history_meta_ttl",id:"history_meta_ttl",level:3},{value:"force_positioning",id:"force_positioning",level:3},{value:"force_recovery",id:"force_recovery",level:3},{value:"force_recovery_mode",id:"force_recovery_mode",level:3},{value:"allow_subscribe_for_client",id:"allow_subscribe_for_client",level:3},{value:"allow_subscribe_for_anonymous",id:"allow_subscribe_for_anonymous",level:3},{value:"allow_publish_for_subscriber",id:"allow_publish_for_subscriber",level:3},{value:"allow_publish_for_client",id:"allow_publish_for_client",level:3},{value:"allow_publish_for_anonymous",id:"allow_publish_for_anonymous",level:3},{value:"allow_history_for_subscriber",id:"allow_history_for_subscriber",level:3},{value:"allow_history_for_client",id:"allow_history_for_client",level:3},{value:"allow_history_for_anonymous",id:"allow_history_for_anonymous",level:3},{value:"allow_presence_for_subscriber",id:"allow_presence_for_subscriber",level:3},{value:"allow_presence_for_client",id:"allow_presence_for_client",level:3},{value:"allow_presence_for_anonymous",id:"allow_presence_for_anonymous",level:3},{value:"allow_user_limited_channels",id:"allow_user_limited_channels",level:3},{value:"channel_regex",id:"channel_regex",level:3},{value:"delta_publish",id:"delta_publish",level:3},{value:"allowed_delta_types",id:"allowed_delta_types",level:3},{value:"proxy_subscribe",id:"proxy_subscribe",level:3},{value:"proxy_publish",id:"proxy_publish",level:3},{value:"proxy_sub_refresh",id:"proxy_sub_refresh",level:3},{value:"proxy_subscribe_stream",id:"proxy_subscribe_stream",level:3},{value:"subscribe_proxy_name",id:"subscribe_proxy_name",level:3},{value:"publish_proxy_name",id:"publish_proxy_name",level:3},{value:"sub_refresh_proxy_name",id:"sub_refresh_proxy_name",level:3},{value:"subscribe_stream_proxy_name",id:"subscribe_stream_proxy_name",level:3},{value:"cache_empty_proxy_name",id:"cache_empty_proxy_name",level:3},{value:"proxy_cache_empty",id:"proxy_cache_empty",level:3},{value:"shared_position_sync",id:"shared_position_sync",level:3},{value:"channel_state_events",id:"channel_state_events",level:3},{value:"subscribe_cel",id:"subscribe_cel",level:3},{value:"publish_cel",id:"publish_cel",level:3},{value:"history_cel",id:"history_cel",level:3},{value:"presence_cel",id:"presence_cel",level:3},{value:"Channel config examples",id:"channel-config-examples",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"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."}),"\n",(0,i.jsx)(n.h2,{id:"what-is-a-channel",children:"What is a channel?"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo operates on a PUB/SUB model - it has publishers and subscribers. A channel serves as a pathway for messages. Clients can subscribe to a channel to receive all the real-time messages published there. Subscribers to a channel may also request information about the channel's online presence or its history."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"pub_sub",src:s(34478).Z+"",width:"2924",height:"1231"})}),"\n",(0,i.jsxs)(n.p,{children:["A channel is simply a string - names like ",(0,i.jsx)(n.code,{children:"news"}),", ",(0,i.jsx)(n.code,{children:"comments"}),", ",(0,i.jsx)(n.code,{children:"personal_feed"})," are examples of valid channel names. However, there are ",(0,i.jsx)(n.a,{href:"#channel-name-rules",children:"predefined rules"})," for these strings, as we will discuss later. You can define different behaviors for a channel using a range of available ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Channels are ephemeral \u2013 there is no need to create them explicitly. Channels are automatically created by Centrifugo as soon as the first client subscribes. Similarly, when the last subscriber leaves, the channel is automatically cleaned up."}),"\n",(0,i.jsxs)(n.p,{children:["A channel can be part of a channel namespace. ",(0,i.jsx)(n.a,{href:"#channel-namespaces",children:"Channel namespacing"})," is a mechanism to define different behaviors for various channels within Centrifugo. Using namespaces is the recommended approach to manage channels \u2013 enabling only those channel options which are necessary for the specific real-time feature you are implementing with Centrifugo."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Ensure you have defined a namespace in the configuration when using channel namespaces. Attempts to subscribe to a channel within an undefined namespace will result in ",(0,i.jsx)(n.a,{href:"/docs/server/codes#unknown-channel",children:"102: unknown channel"})," errors."]})}),"\n",(0,i.jsx)(n.h2,{id:"channel-name-rules",children:"Channel name rules"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Only ASCII symbols must be used in a channel string"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Channel name length limited by ",(0,i.jsx)(n.code,{children:"255"})," characters by default (controlled by configuration option ",(0,i.jsx)(n.code,{children:"channel_max_length"}),")."]}),"\n",(0,i.jsx)(n.p,{children:"Several symbols in channel names reserved for Centrifugo internal needs:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:":"})," \u2013 for namespace channel boundary (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"#"})," \u2013 for user channel boundary (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"$"})," \u2013 for private channel prefix (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"/"})," \u2013 for ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"Channel Patterns"})," in Centrifugo PRO"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"*"})," \u2013 for the future Centrifugo needs"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"&"})," \u2013 for the future Centrifugo needs"]}),"\n"]}),"\n",(0,i.jsxs)(n.h3,{id:"namespace-boundary-",children:["namespace boundary (",(0,i.jsx)(n.code,{children:":"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:":"})," \u2013 is a channel namespace boundary. Namespaces are used to set custom options to a group of channels. Each channel belonging to the same namespace will have the same channel options. Read more about ",(0,i.jsx)(n.a,{href:"#channel-namespaces",children:"namespaces"})," and ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"})," below."]}),"\n",(0,i.jsxs)(n.p,{children:["If the channel is ",(0,i.jsx)(n.code,{children:"public:chat"})," - then Centrifugo will apply options to this channel from the channel namespace with the name ",(0,i.jsx)(n.code,{children:"public"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["A namespace is an inalienable component of the channel name. If a user is subscribed to a channel with a namespace, such as ",(0,i.jsx)(n.code,{children:"public:chat"}),", then you must publish messages to the ",(0,i.jsx)(n.code,{children:"public:chat"})," channel for them to be delivered to the user. There is often confusion among developers who try to publish messages to ",(0,i.jsx)(n.code,{children:"chat"}),", mistakenly believing that the namespace is stripped upon subscription. This is not the case. You must publish exactly to the same channel string you used for subscribing."]})}),"\n",(0,i.jsxs)(n.h3,{id:"user-channel-boundary-",children:["user channel boundary (",(0,i.jsx)(n.code,{children:"#"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"#"})," symbol serves as the user channel boundary. It acts as a separator to create personal channels for users, known as ",(0,i.jsx)(n.em,{children:"user-limited channels"}),", without requiring a subscription token."]}),"\n",(0,i.jsxs)(n.p,{children:["For instance, if the channel is named ",(0,i.jsx)(n.code,{children:"news#42"}),", then only the user with ID ",(0,i.jsx)(n.code,{children:"42"})," can subscribe to this channel. Centrifugo identifies the user ID from the connection credentials provided in the connection JWT."]}),"\n",(0,i.jsxs)(n.p,{children:["To create a user-limited channel within the ",(0,i.jsx)(n.code,{children:"personal"})," namespace, you might use a name such as ",(0,i.jsx)(n.code,{children:"personal:user#42"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Furthermore, it's possible to specify multiple user IDs in the channel name, separated by a comma: ",(0,i.jsx)(n.code,{children:"dialog#42,43"}),". In this case, only users with IDs ",(0,i.jsx)(n.code,{children:"42"})," and ",(0,i.jsx)(n.code,{children:"43"})," are permitted to subscribe to this channel."]}),"\n",(0,i.jsx)(n.p,{children:"This setup is ideal for channels that have a static list of allowed users, such as channels for personal messages to a single user or dialogue channels between specific users. However, for dynamic access management of a channel for numerous users, this type of channel is not appropriate."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["User-limited channels must be enabled for a channel namespace using ",(0,i.jsx)(n.a,{href:"#allow_user_limited_channels",children:"allow_user_limited_channels"})," option. See below more information about channel options and channel namespaces."]})}),"\n",(0,i.jsxs)(n.h3,{id:"private-channel-prefix-",children:["private channel prefix (",(0,i.jsx)(n.code,{children:"$"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo maintains compatibility with its previous versions which had concept of private channels. In earlier versions \u2014 specifically Centrifugo v1, v2, and v3\u2014only \u2013 only channels beginning with ",(0,i.jsx)(n.code,{children:"$"})," required a subscription JWT for subscribing. With Centrifugo v4, this is no longer the case; clients can subscribe to any channel if they have a valid subscription token."]}),"\n",(0,i.jsxs)(n.p,{children:["However, for namespaces where the ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," option is activated, Centrifugo prohibits subscriptions to channels that start with the ",(0,i.jsx)(n.code,{children:"private_channel_prefix"})," (which defaults to ",(0,i.jsx)(n.code,{children:"$"}),") unless a subscription token is provided. This restriction is designed to facilitate a secure migration to Centrifugo v4 or later versions."]}),"\n",(0,i.jsx)(n.h3,{id:"channel-is-just-a-string",children:"Channel is just a string"}),"\n",(0,i.jsxs)(n.p,{children:["Remember that a channel is uniquely identified by its string name. Do not assume that ",(0,i.jsx)(n.code,{children:"$news"})," and ",(0,i.jsx)(n.code,{children:"news"})," are the same; they are different because their names are not identical. Therefore, if a user is subscribed to ",(0,i.jsx)(n.code,{children:"$news"}),", they will not receive messages published to ",(0,i.jsx)(n.code,{children:"news"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["The channels ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," and ",(0,i.jsx)(n.code,{children:"dialog#43,42"})," are considered different as well. Centrifugo only applies permission checks when a user subscribes to a channel. So if user-limited channels are enabled then the user with ID ",(0,i.jsx)(n.code,{children:"42"})," will be able to subscribe on both ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," and ",(0,i.jsx)(n.code,{children:"dialog#43,42"}),". But Centrifugo does no magic regarding channel strings when keeping channel->to->subscribers map. So if the user subscribed on ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," you must publish messages to exactly that channel: ",(0,i.jsx)(n.code,{children:"dialog#42,43"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["The same reasoning applies to channels within namespaces. Channels ",(0,i.jsx)(n.code,{children:"chat:index"})," and ",(0,i.jsx)(n.code,{children:"index"})," are not the same \u2014 they are distinct and, moreover, they belong to different namespaces. The concept of channel namespaces in Centrifugo will be discussed shortly."]}),"\n",(0,i.jsx)(n.h2,{id:"channel-namespaces",children:"Channel namespaces"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo allows configuring a list of channel namespaces. Namespaces are optional but super-useful."}),"\n",(0,i.jsxs)(n.p,{children:["A namespace is a container for options applied to channels that start with the namespace name + ",(0,i.jsx)(n.code,{children:":"})," separator. For example, if you define a namespace named ",(0,i.jsx)(n.code,{children:"personal"})," in the configuration, all channels starting with ",(0,i.jsx)(n.code,{children:"personal:"})," (such as ",(0,i.jsx)(n.code,{children:"personal:1"})," or ",(0,i.jsx)(n.code,{children:"personal:2"}),") will inherit the options defined for the ",(0,i.jsx)(n.code,{children:"personal"})," namespace. This gives you great control over channel behavior, allowing you to set different options for various real-time features in your application."]}),"\n",(0,i.jsxs)(n.p,{children:["Namespace has a name, and can contain all the ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"}),". Namespace ",(0,i.jsx)(n.code,{children:"name"})," is required to be set. Name of namespace must be unique, must consist of letters, numbers, underscores, or hyphens and be more than 2 symbols length i.e. satisfy regexp ",(0,i.jsx)(n.code,{children:"^[-a-zA-Z0-9_]{2,}$"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When you want to use specific namespace options your channel must be prefixed with namespace name and ",(0,i.jsx)(n.code,{children:":"})," separator: ",(0,i.jsx)(n.code,{children:"public:messages"}),", ",(0,i.jsx)(n.code,{children:"gossips:messages"})," are two channels in ",(0,i.jsx)(n.code,{children:"public"})," and ",(0,i.jsx)(n.code,{children:"gossips"})," namespaces."]}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo looks for ",(0,i.jsx)(n.code,{children:":"})," symbol in the channel name, if found \u2013 extracts the namespace name, and applies all the configured namespace channel options while processing protocol commands from a client or server API calls."]}),"\n",(0,i.jsxs)(n.p,{children:["All things together here is an example of ",(0,i.jsx)(n.code,{children:"config.json"})," which includes some top-level channel options set and has 2 additional channel namespaces configured:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "very-long-secret-key",\n "api_key": "secret-api-key",\n \n "presence": true,\n "history_size": 10,\n "history_ttl": "30s",\n \n "namespaces": [\n {\n "name": "facts",\n "history_size": 10,\n "history_ttl": "300s"\n },\n {\n "name": "gossips"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"news"})," will use globally defined channel options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"facts:sport"})," will use ",(0,i.jsx)(n.code,{children:"facts"})," namespace options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"gossips:sport"})," will use ",(0,i.jsx)(n.code,{children:"gossips"})," namespace options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"xxx:hello"})," will result into subscription error since there is no ",(0,i.jsx)(n.code,{children:"xxx"})," namespace defined in the configuration above."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Channel namespaces also work with private channels and user-limited channels"}),". For example, if you have a namespace called ",(0,i.jsx)(n.code,{children:"dialogs"})," then the private channel can be constructed as ",(0,i.jsx)(n.code,{children:"$dialogs:gossips"}),", user-limited channel can be constructed as ",(0,i.jsx)(n.code,{children:"dialogs:dialog#1,2"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["There is ",(0,i.jsx)(n.strong,{children:"no inheritance"})," in channel options and namespaces \u2013 for example, you defined ",(0,i.jsx)(n.code,{children:"presence: true"})," on a top level of configuration and then defined a namespace \u2013 that namespace won't have online presence enabled - you must enable it for a namespace explicitly."]})}),"\n",(0,i.jsx)(n.p,{children:"There are many options which can be set for channel namespace (on top-level and to named one) to modify behavior of channels belonging to a namespace. Below we describe all these options."}),"\n",(0,i.jsx)(n.h2,{id:"channel-options",children:"Channel options"}),"\n",(0,i.jsx)(n.p,{children:"Channel behavior can be modified by using channel options. Channel options can be defined on configuration top-level and for every namespace."}),"\n",(0,i.jsx)(n.h3,{id:"presence",children:"presence"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"presence"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 enable/disable online presence information for channels in a namespace."]}),"\n",(0,i.jsx)(n.p,{children:"Online presence is information about clients currently subscribed to the channel. It contains each subscriber's client ID, user ID, connection info, and channel info. By default, this option is off so no presence information will be available for channels."}),"\n",(0,i.jsxs)(n.p,{children:["Let's say you have a channel ",(0,i.jsx)(n.code,{children:"chat:index"})," with two users subscribed (IDs ",(0,i.jsx)(n.code,{children:"2694"})," and ",(0,i.jsx)(n.code,{children:"56"}),"). User ",(0,i.jsx)(n.code,{children:"56"})," has one connection to Centrifugo. User ",(0,i.jsx)(n.code,{children:"2694"})," has two connections to Centrifugo from different browser tabs. The presence data might look like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: " \\\n --request POST \\\n --data \'{"channel": "chat:index"}\' \\\n http://localhost:8000/api/presence\n{\n "result": {\n "presence": {\n "66fdf8d1-06f0-4375-9fac-db959d6ee8d6": {\n "user": "2694",\n "client": "66fdf8d1-06f0-4375-9fac-db959d6ee8d6",\n "conn_info": {"name": "Alex"}\n },\n "d4516dd3-0b6e-4cfe-84e8-0342fd2bb20c": {\n "user": "2694",\n "client": "d4516dd3-0b6e-4cfe-84e8-0342fd2bb20c",\n "conn_info": {"name": "Alex"}\n }\n "g3216dd3-1b6e-tcfe-14e8-1342fd2bb20c": {\n "user": "56",\n "client": "g3216dd3-1b6e-tcfe-14e8-1342fd2bb20c",\n "conn_info": {"name": "Alice"}\n }\n }\n }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To call presence API from the client connection side client must have permission to do so. See ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#presence-permission-model",children:"presence permission model"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Enabling channel online presence adds some overhead since Centrifugo needs to maintain an additional data structure (in a process memory or in a broker memory/disk). So only use it for channels where presence is required."})}),"\n",(0,i.jsxs)(n.p,{children:["See more details about ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#online-presence-considerations",children:"online presence design"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"join_leave",children:"join_leave"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"join_leave"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 enable/disable sending join and leave messages when the client subscribes to a channel (unsubscribes from a channel). Join/leave event includes information about the connection that triggered an event \u2013 client ID, user ID, connection info, and channel info (similar to entry inside presence information)."]}),"\n",(0,i.jsxs)(n.p,{children:["Enabling ",(0,i.jsx)(n.code,{children:"join_leave"})," means that Join/Leave messages will start being emitted, but by default they are not delivered to clients subscribed to a channel. You need to force this using namespace option ",(0,i.jsx)(n.a,{href:"#forcepushjoinleave",children:"force_push_join_leave"})," or explicitly provide intent from a client-side (in this case client must have permission to call presence API)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:'Keep in mind that join/leave messages can generate a huge number of messages in a system if turned on for channels with a large number of active subscribers. If you have channels with a large number of subscribers consider avoiding using this feature. It\'s hard to say what is "large" for you though \u2013 just estimate the load based on the fact that each subscribe/unsubscribe event in a channel with N subscribers will result into N messages broadcasted to all. If all clients reconnect at the same time the amount of generated messages is N^2.'})}),"\n",(0,i.jsx)(n.p,{children:"Join/leave messages distributed only with at most once delivery guarantee."}),"\n",(0,i.jsx)(n.h3,{id:"force_push_join_leave",children:"force_push_join_leave"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"When on all clients will receive join/leave events for a channel in a namespace automatically \u2013 without explicit intent to consume join/leave messages from the client side."}),"\n",(0,i.jsx)(n.p,{children:"If pushing join/leave is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel presence (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.h3,{id:"history_size",children:"history_size"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_size"})," (integer, default ",(0,i.jsx)(n.code,{children:"0"}),") \u2013 history size (amount of messages) for channels. As Centrifugo keeps all history messages in process memory (or in a broker memory) it's very important to limit the maximum amount of messages in channel history with a reasonable value. ",(0,i.jsx)(n.code,{children:"history_size"})," defines the maximum amount of messages that Centrifugo will keep for ",(0,i.jsx)(n.strong,{children:"each"})," channel in the namespace. As soon as history has more messages than defined by history size \u2013 old messages will be evicted."]}),"\n",(0,i.jsxs)(n.p,{children:["Setting only ",(0,i.jsx)(n.code,{children:"history_size"})," ",(0,i.jsx)(n.strong,{children:"is not enough to enable history in channels"})," \u2013 you also need to wisely configure ",(0,i.jsx)(n.code,{children:"history_ttl"})," option (see below)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Enabling channel history adds some overhead (both memory and CPU) since Centrifugo needs to maintain an additional data structure (in a process memory or a broker memory/disk). So only use history for channels where it's required."})}),"\n",(0,i.jsx)(n.h3,{id:"history_ttl",children:"history_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),", default ",(0,i.jsx)(n.code,{children:"0s"}),") \u2013 interval how long to keep channel history messages (with seconds precision)."]}),"\n",(0,i.jsx)(n.p,{children:"As all history is storing in process memory (or in a broker memory) it is also very important to get rid of old history data for unused (inactive for a long time) channels."}),"\n",(0,i.jsx)(n.p,{children:"By default history TTL duration is zero \u2013 this means that channel history is disabled."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Again \u2013 to turn on history you should wisely configure both ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," options"]}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Also note, that ",(0,i.jsx)(n.code,{children:"history_ttl"})," must be less than ",(0,i.jsx)(n.a,{href:"#history_meta_ttl",children:"history_meta_ttl"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"For example for top-level channels (which do not belong to a namespace):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "history_size": 10,\n "history_ttl": "60s"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Here's an example. You enabled history for the ",(0,i.jsx)(n.code,{children:"chat"})," namespace and sent two messages in the ",(0,i.jsx)(n.code,{children:"chat:index"})," channel. The history will look like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: " \\\n --request POST \\\n --data \'{"channel": "chat:index", "limit": 100}\' \\\n http://localhost:8000/api/history\n{\n "result": {\n "publications": [\n {\n "data": {\n "input": "1"\n },\n "offset": 1\n },\n {\n "data": {\n "input": "2"\n },\n "offset": 2\n }\n ],\n "epoch": "gWuY",\n "offset": 2\n }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To call the history API from the client side, the client must have the necessary permissions. For more details, see the ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#history-permission-model",children:"history permission model"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["See additional information about offsets and epoch in ",(0,i.jsx)(n.a,{href:"/docs/server/history_and_recovery",children:"History and recovery"})," chapter."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"The persistence properties of history data depend on the Centrifugo engine in use. For instance, with the Memory engine (default), history is retained only until the Centrifugo node restarts. In contrast, with the Redis engine, persistence is determined by the Redis server's configuration (similarly for Redis-compatible storages and Tarantool)."})}),"\n",(0,i.jsx)(n.h3,{id:"history_meta_ttl",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),") \u2013 sets a time of history stream metadata expiration (with seconds precision)."]}),"\n",(0,i.jsxs)(n.p,{children:["If not specified Centrifugo namespace inherits value from ",(0,i.jsx)(n.code,{children:"global_history_meta_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),") option which is 30 days by default (",(0,i.jsx)(n.code,{children:'"720h"'}),"). This should be a good default for most use cases to avoid tweaking ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," on a namespace level at all. If you have ",(0,i.jsx)(n.code,{children:"history_ttl"})," greater than 30 days \u2013 then increase ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," for namespace (recommended) or increase ",(0,i.jsx)(n.code,{children:"global_history_meta_ttl"})," to be larger than ",(0,i.jsx)(n.code,{children:"history_ttl"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"The motivation to have history meta information TTL is as follows. When using a history in a channel, Centrifugo keeps some metadata for each channel stream. Metadata includes the latest stream offset and its epoch value. In some cases, when channels are created for \u0430 short time and then not used anymore, created metadata can stay in memory while not useful. For example, you can have a personal user channel but after using your app for a while user left it forever. From a long-term perspective, this can be an unwanted memory growth. Setting a reasonable value to this option can help to expire metadata faster (or slower if you need it). The rule of thumb here is to keep this value larger than history TTL used."}),"\n",(0,i.jsx)(n.h3,{id:"force_positioning",children:"force_positioning"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_positioning"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when the ",(0,i.jsx)(n.code,{children:"force_positioning"})," option is on Centrifugo forces all subscriptions in a namespace to be ",(0,i.jsx)(n.code,{children:"positioned"}),". I.e. Centrifugo will try to compensate at most once delivery of PUB/SUB broker checking client position inside a stream."]}),"\n",(0,i.jsxs)(n.p,{children:["If Centrifugo detects a bad position of the client (i.e. potential message loss) it disconnects a client with the ",(0,i.jsx)(n.code,{children:"Insufficient state"})," disconnect code. Also, when the position option is enabled Centrifugo exposes the current stream top ",(0,i.jsx)(n.code,{children:"offset"})," and current ",(0,i.jsx)(n.code,{children:"epoch"})," in subscribe reply making it possible for a client to manually recover its state upon disconnect using history API."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_positioning"})," option must be used in conjunction with reasonably configured message history for a channel i.e. ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," ",(0,i.jsx)(n.strong,{children:"must be set"})," (because Centrifugo uses channel history to check client position in a stream)."]}),"\n",(0,i.jsx)(n.p,{children:"If positioning is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel history (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.h3,{id:"force_recovery",children:"force_recovery"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when the ",(0,i.jsx)(n.code,{children:"force_recovery"})," option is on Centrifugo forces all subscriptions in a namespace to be ",(0,i.jsx)(n.code,{children:"recoverable"}),". When enabled Centrifugo will try to recover missed publications in channels after a client reconnects for some reason (bad internet connection for example). Also when the recovery feature is on Centrifugo automatically enables properties of the ",(0,i.jsx)(n.code,{children:"force_positioning"})," option described above."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery"})," option must be used in conjunction with reasonably configured message history for channel i.e. ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," ",(0,i.jsx)(n.strong,{children:"must be set"})," (because Centrifugo uses channel history to recover messages)."]}),"\n",(0,i.jsx)(n.p,{children:"If recovery is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel history (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Not all real-time events require this feature turned on so think wisely when you need this. When this option is turned on your application should be designed in a way to tolerate duplicate messages coming from a channel (currently Centrifugo returns recovered publications in order and without duplicates but this is an implementation detail that can be theoretically changed in the future). See more details about how recovery works in ",(0,i.jsx)(n.a,{href:"/docs/server/history_and_recovery",children:"special chapter"}),"."]})}),"\n",(0,i.jsx)(n.h3,{id:"force_recovery_mode",children:"force_recovery_mode"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery_mode"})," (string, possible values are ",(0,i.jsx)(n.code,{children:"stream"})," or ",(0,i.jsx)(n.code,{children:"cache"}),", when not specified Centrifugo uses ",(0,i.jsx)(n.code,{children:'"stream"'}),"). Allows setting recovery mode for all connections which use recovery in the namespace. By default, Centrifugo uses ",(0,i.jsx)(n.code,{children:"stream"})," recovery mode \u2013 a mode where subscriber interested in all messages to be delivered. The alternative recovery mode which may be forced by using this option is ",(0,i.jsx)(n.code,{children:"cache"})," \u2013 see the detailed description in ",(0,i.jsx)(n.a,{href:"/docs/server/cache_recovery",children:"Cache recovery mode"})," chapter."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_subscribe_for_client",children:"allow_subscribe_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when on all non-anonymous clients will be able to subscribe to any channel in a namespace. To additionally allow anonymous users to subscribe turn on ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_anonymous"})," (see below)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Turning this option on effectively makes namespace public \u2013 no subscribe permissions will be checked (only the check that current connection is authenticated - i.e. has non-empty user ID). Make sure this is really what you want in terms of channels security."})}),"\n",(0,i.jsx)(n.h3,{id:"allow_subscribe_for_anonymous",children:"allow_subscribe_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_subscribe_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients (with empty user ID) should be able to subscribe on channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_subscriber",children:"allow_publish_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - when the ",(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," option is enabled client can publish into a channel in namespace directly from the client side over real-time connection but only if client subscribed to that channel."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Keep in mind that in this case subscriber can publish any payload to a channel \u2013 Centrifugo does not validate input at all. Your app backend won't receive those messages - publications just go through Centrifugo towards channel subscribers. Consider always validate messages which are being published to channels (i.e. using server API to publish after validating input on the backend side, or using ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#publish-proxy",children:"publish proxy"})," - see ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#idiomatic-usage",children:"idiomatic usage"}),")."]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," (or ",(0,i.jsx)(n.code,{children:"allow_publish_for_client"})," mentioned below) option still can be useful to send something without backend-side validation and saving it into a database \u2013 for example, this option may be handy for demos and quick prototyping real-time app ideas."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_client",children:"allow_publish_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when on allows clients to publish messages into channels directly (from a client-side). It's like ",(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," \u2013 but client should not be a channel subscriber to publish."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Keep in mind that in this case client can publish any payload to a channel \u2013 Centrifugo does not validate input at all. Your app backend won't receive those messages - publications just go through Centrifugo towards channel subscribers. Consider always validate messages which are being published to channels (i.e. using server API to publish after validating input on the backend side, or using ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#publish-proxy",children:"publish proxy"})," - see ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#idiomatic-usage",children:"idiomatic usage"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_anonymous",children:"allow_publish_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to publish into channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_subscriber",children:"allow_history_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows clients who subscribed on a channel to call history API from that channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_client",children:"allow_history_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows all clients to call history information in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_anonymous",children:"allow_history_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to call history from channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_subscriber",children:"allow_presence_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows clients who subscribed on a channel to call presence information from that channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_client",children:"allow_presence_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows all clients to call presence information in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_anonymous",children:"allow_presence_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to call presence from channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_user_limited_channels",children:"allow_user_limited_channels"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_user_limited_channels"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - allows using user-limited channels in a namespace for checking subscribe permission."]}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["If client subscribes to a user-limited channel while this option is off then server rejects subscription with ",(0,i.jsx)(n.code,{children:"103: permission denied"})," error."]})}),"\n",(0,i.jsx)(n.h3,{id:"channel_regex",children:"channel_regex"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"channel_regex"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 is an option to set a regular expression for channels allowed in the namespace. By default Centrifugo does not limit channel name variations. For example, if you have a namespace ",(0,i.jsx)(n.code,{children:"chat"}),", then channel names inside this namespace are not really limited, it can be ",(0,i.jsx)(n.code,{children:"chat:index"}),", ",(0,i.jsx)(n.code,{children:"chat:1"}),", ",(0,i.jsx)(n.code,{children:"chat:2"}),", ",(0,i.jsx)(n.code,{children:"chat:zzz"})," and so on. But if you want to be strict and know possible channel patterns you can use ",(0,i.jsx)(n.code,{children:"channel_regex"})," option. This is especially useful in namespaces where all clients can subscribe to channels."]}),"\n",(0,i.jsxs)(n.p,{children:["For example, let's only allow digits after ",(0,i.jsx)(n.code,{children:"chat:"})," for channel names in a ",(0,i.jsx)(n.code,{children:"chat"})," namespace:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "chat",\n "allow_subscribe_for_client": true,\n "channel_regex": "^[\\d+]$"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Note, that we are skipping ",(0,i.jsx)(n.code,{children:"chat:"})," part in regex. Since namespace prefix is the same for all channels in a namespace we only match the rest (after the prefix) of channel name."]})}),"\n",(0,i.jsx)(n.p,{children:"Channel regex only checked for client-side subscriptions, if you are using server-side subscriptions Centrifugo won't check the regex."}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo uses Go language ",(0,i.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"regexp"})," package for regular expressions."]}),"\n",(0,i.jsx)(n.h3,{id:"delta_publish",children:"delta_publish"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"delta_publish"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") allows marking all publications in the namespace with ",(0,i.jsx)(n.code,{children:"delta"})," flag, i.e. all publications will result into delta updates for subscribers which negotiated delta compression for a channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allowed_delta_types",children:"allowed_delta_types"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allowed_delta_types"})," (array of strings, the only allowed value now is ",(0,i.jsx)(n.code,{children:"fossil"}),") - provide an array of allowed delta compression types in the namespace. If not specified \u2013 client won't be able to negotiate delta compression in channels."]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_subscribe",children:"proxy_subscribe"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_subscribe"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on subscribe proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_publish",children:"proxy_publish"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_publish"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on publish proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_sub_refresh",children:"proxy_sub_refresh"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_sub_refresh"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on sub refresh proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_subscribe_stream",children:"proxy_subscribe_stream"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_subscribe_stream"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - turns on subscribe stream proxy, see ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams",children:"subscription streams"})]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_proxy_name",children:"subscribe_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on subscribe proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_subscribe"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"publish_proxy_name",children:"publish_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"publish_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on publish proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_publish"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"sub_refresh_proxy_name",children:"sub_refresh_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"sub_refresh_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on sub refresh proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_sub_refresh"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_stream_proxy_name",children:"subscribe_stream_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_stream_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on subscribe stream proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_subscribe_stream"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"cache_empty_proxy_name",children:"cache_empty_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"cache_empty_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 turns on ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_cache_empty",children:"cache empty proxy"})," when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_cache_empty"})," option is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_cache_empty",children:"proxy_cache_empty"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_cache_empty"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),", Centrifugo PRO only) - turns on cache empty proxy, see ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_cache_empty",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"shared_position_sync",children:"shared_position_sync"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"shared_position_sync"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),", Centrifugo PRO only) - can help reducing the number of position synchronization requests from Centrifugo to Broker's history API, see ",(0,i.jsx)(n.a,{href:"/docs/pro/engine_optimizations#shared-position-sync",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"channel_state_events",children:"channel_state_events"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"channel_state_events"})," (array of strings, empty by default, Centrifugo PRO only) - can help configuring notifications about channel's ",(0,i.jsx)(n.code,{children:"occupied"})," and ",(0,i.jsx)(n.code,{children:"vacated"})," state. See ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_state_events",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_cel",children:"subscribe_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for subscribe permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"publish_cel",children:"publish_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"publish_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for publish permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"history_cel",children:"history_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for history permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"presence_cel",children:"presence_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"presence_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for presence permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h2,{id:"channel-config-examples",children:"Channel config examples"}),"\n",(0,i.jsx)(n.p,{children:"Let's look at how to set some of these options in a config. In this example we turning on presence, history features, forcing publication recovery. Also allowing all client connections (including anonymous users) to subscribe to channels and call publish, history, presence APIs if subscribed."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "my-secret-key",\n "api_key": "secret-api-key",\n "presence": true,\n "history_size": 10,\n "history_ttl": "300s",\n "force_recovery": true,\n "allow_subscribe_for_client": true,\n "allow_subscribe_for_anonymous": true,\n "allow_publish_for_subscriber": true,\n "allow_publish_for_anonymous": true,\n "allow_history_for_subscriber": true,\n "allow_history_for_anonymous": true,\n "allow_presence_for_subscriber": true,\n "allow_presence_for_anonymous": true\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Here we set channel options on config top-level \u2013 these options will affect channels without namespace. In many cases defining namespaces is a recommended approach so you can manage options for every real-time feature separately. With namespaces the above config may transform to:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "my-secret-key",\n "api_key": "secret-api-key",\n "namespaces": [\n {\n "name": "feed",\n "presence": true,\n "history_size": 10,\n "history_ttl": "300s",\n "force_recovery": true,\n "allow_subscribe_for_client": true,\n "allow_subscribe_for_anonymous": true,\n "allow_publish_for_subscriber": true,\n "allow_publish_for_anonymous": true,\n "allow_history_for_subscriber": true,\n "allow_history_for_anonymous": true,\n "allow_presence_for_subscriber": true,\n "allow_presence_for_anonymous": true\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["In this case channels should be prefixed with ",(0,i.jsx)(n.code,{children:"feed:"})," to follow the behavior configured for a ",(0,i.jsx)(n.code,{children:"feed"})," namespace."]})]})}function d(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},34478:(e,n,s)=>{s.d(n,{Z:()=>i});const i=s.p+"assets/images/pub_sub-5477abf6fb38219fc0848d9c9c3dc2b1.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>l,a:()=>a});var i=s(67294);const o={},r=i.createContext(o);function a(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8896],{31929:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>d,frontMatter:()=>r,metadata:()=>l,toc:()=>t});var i=s(85893),o=s(11151);const r={id:"channels",title:"Channels and namespaces"},a=void 0,l={id:"server/channels",title:"Channels and namespaces",description:"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.",source:"@site/docs/server/channels.md",sourceDirName:"server",slug:"/server/channels",permalink:"/docs/server/channels",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/channels.md",tags:[],version:"current",frontMatter:{id:"channels",title:"Channels and namespaces"},sidebar:"Guides",previous:{title:"Client JWT authentication",permalink:"/docs/server/authentication"},next:{title:"Channel permission model",permalink:"/docs/server/channel_permissions"}},c={},t=[{value:"What is a channel?",id:"what-is-a-channel",level:2},{value:"Channel name rules",id:"channel-name-rules",level:2},{value:"namespace boundary (:)",id:"namespace-boundary-",level:3},{value:"user channel boundary (#)",id:"user-channel-boundary-",level:3},{value:"private channel prefix ($)",id:"private-channel-prefix-",level:3},{value:"Channel is just a string",id:"channel-is-just-a-string",level:3},{value:"Channel namespaces",id:"channel-namespaces",level:2},{value:"Channel options",id:"channel-options",level:2},{value:"presence",id:"presence",level:3},{value:"join_leave",id:"join_leave",level:3},{value:"force_push_join_leave",id:"force_push_join_leave",level:3},{value:"history_size",id:"history_size",level:3},{value:"history_ttl",id:"history_ttl",level:3},{value:"history_meta_ttl",id:"history_meta_ttl",level:3},{value:"force_positioning",id:"force_positioning",level:3},{value:"force_recovery",id:"force_recovery",level:3},{value:"force_recovery_mode",id:"force_recovery_mode",level:3},{value:"allow_subscribe_for_client",id:"allow_subscribe_for_client",level:3},{value:"allow_subscribe_for_anonymous",id:"allow_subscribe_for_anonymous",level:3},{value:"allow_publish_for_subscriber",id:"allow_publish_for_subscriber",level:3},{value:"allow_publish_for_client",id:"allow_publish_for_client",level:3},{value:"allow_publish_for_anonymous",id:"allow_publish_for_anonymous",level:3},{value:"allow_history_for_subscriber",id:"allow_history_for_subscriber",level:3},{value:"allow_history_for_client",id:"allow_history_for_client",level:3},{value:"allow_history_for_anonymous",id:"allow_history_for_anonymous",level:3},{value:"allow_presence_for_subscriber",id:"allow_presence_for_subscriber",level:3},{value:"allow_presence_for_client",id:"allow_presence_for_client",level:3},{value:"allow_presence_for_anonymous",id:"allow_presence_for_anonymous",level:3},{value:"allow_user_limited_channels",id:"allow_user_limited_channels",level:3},{value:"channel_regex",id:"channel_regex",level:3},{value:"delta_publish",id:"delta_publish",level:3},{value:"allowed_delta_types",id:"allowed_delta_types",level:3},{value:"proxy_subscribe",id:"proxy_subscribe",level:3},{value:"proxy_publish",id:"proxy_publish",level:3},{value:"proxy_sub_refresh",id:"proxy_sub_refresh",level:3},{value:"proxy_subscribe_stream",id:"proxy_subscribe_stream",level:3},{value:"subscribe_proxy_name",id:"subscribe_proxy_name",level:3},{value:"publish_proxy_name",id:"publish_proxy_name",level:3},{value:"sub_refresh_proxy_name",id:"sub_refresh_proxy_name",level:3},{value:"subscribe_stream_proxy_name",id:"subscribe_stream_proxy_name",level:3},{value:"cache_empty_proxy_name",id:"cache_empty_proxy_name",level:3},{value:"proxy_cache_empty",id:"proxy_cache_empty",level:3},{value:"shared_position_sync",id:"shared_position_sync",level:3},{value:"channel_state_events",id:"channel_state_events",level:3},{value:"subscribe_cel",id:"subscribe_cel",level:3},{value:"publish_cel",id:"publish_cel",level:3},{value:"history_cel",id:"history_cel",level:3},{value:"presence_cel",id:"presence_cel",level:3},{value:"Channel config examples",id:"channel-config-examples",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"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."}),"\n",(0,i.jsx)(n.h2,{id:"what-is-a-channel",children:"What is a channel?"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo operates on a PUB/SUB model - it has publishers and subscribers. A channel serves as a pathway for messages. Clients can subscribe to a channel to receive all the real-time messages published there. Subscribers to a channel may also request information about the channel's online presence or its history."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"pub_sub",src:s(88550).Z+"",width:"2924",height:"1231"})}),"\n",(0,i.jsxs)(n.p,{children:["A channel is simply a string - names like ",(0,i.jsx)(n.code,{children:"news"}),", ",(0,i.jsx)(n.code,{children:"comments"}),", ",(0,i.jsx)(n.code,{children:"personal_feed"})," are examples of valid channel names. However, there are ",(0,i.jsx)(n.a,{href:"#channel-name-rules",children:"predefined rules"})," for these strings, as we will discuss later. You can define different behaviors for a channel using a range of available ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Channels are ephemeral \u2013 there is no need to create them explicitly. Channels are automatically created by Centrifugo as soon as the first client subscribes. Similarly, when the last subscriber leaves, the channel is automatically cleaned up."}),"\n",(0,i.jsxs)(n.p,{children:["A channel can be part of a channel namespace. ",(0,i.jsx)(n.a,{href:"#channel-namespaces",children:"Channel namespacing"})," is a mechanism to define different behaviors for various channels within Centrifugo. Using namespaces is the recommended approach to manage channels \u2013 enabling only those channel options which are necessary for the specific real-time feature you are implementing with Centrifugo."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Ensure you have defined a namespace in the configuration when using channel namespaces. Attempts to subscribe to a channel within an undefined namespace will result in ",(0,i.jsx)(n.a,{href:"/docs/server/codes#unknown-channel",children:"102: unknown channel"})," errors."]})}),"\n",(0,i.jsx)(n.h2,{id:"channel-name-rules",children:"Channel name rules"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Only ASCII symbols must be used in a channel string"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Channel name length limited by ",(0,i.jsx)(n.code,{children:"255"})," characters by default (controlled by configuration option ",(0,i.jsx)(n.code,{children:"channel_max_length"}),")."]}),"\n",(0,i.jsx)(n.p,{children:"Several symbols in channel names reserved for Centrifugo internal needs:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:":"})," \u2013 for namespace channel boundary (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"#"})," \u2013 for user channel boundary (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"$"})," \u2013 for private channel prefix (see below)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"/"})," \u2013 for ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"Channel Patterns"})," in Centrifugo PRO"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"*"})," \u2013 for the future Centrifugo needs"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"&"})," \u2013 for the future Centrifugo needs"]}),"\n"]}),"\n",(0,i.jsxs)(n.h3,{id:"namespace-boundary-",children:["namespace boundary (",(0,i.jsx)(n.code,{children:":"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:":"})," \u2013 is a channel namespace boundary. Namespaces are used to set custom options to a group of channels. Each channel belonging to the same namespace will have the same channel options. Read more about ",(0,i.jsx)(n.a,{href:"#channel-namespaces",children:"namespaces"})," and ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"})," below."]}),"\n",(0,i.jsxs)(n.p,{children:["If the channel is ",(0,i.jsx)(n.code,{children:"public:chat"})," - then Centrifugo will apply options to this channel from the channel namespace with the name ",(0,i.jsx)(n.code,{children:"public"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsxs)(n.p,{children:["A namespace is an inalienable component of the channel name. If a user is subscribed to a channel with a namespace, such as ",(0,i.jsx)(n.code,{children:"public:chat"}),", then you must publish messages to the ",(0,i.jsx)(n.code,{children:"public:chat"})," channel for them to be delivered to the user. There is often confusion among developers who try to publish messages to ",(0,i.jsx)(n.code,{children:"chat"}),", mistakenly believing that the namespace is stripped upon subscription. This is not the case. You must publish exactly to the same channel string you used for subscribing."]})}),"\n",(0,i.jsxs)(n.h3,{id:"user-channel-boundary-",children:["user channel boundary (",(0,i.jsx)(n.code,{children:"#"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"#"})," symbol serves as the user channel boundary. It acts as a separator to create personal channels for users, known as ",(0,i.jsx)(n.em,{children:"user-limited channels"}),", without requiring a subscription token."]}),"\n",(0,i.jsxs)(n.p,{children:["For instance, if the channel is named ",(0,i.jsx)(n.code,{children:"news#42"}),", then only the user with ID ",(0,i.jsx)(n.code,{children:"42"})," can subscribe to this channel. Centrifugo identifies the user ID from the connection credentials provided in the connection JWT."]}),"\n",(0,i.jsxs)(n.p,{children:["To create a user-limited channel within the ",(0,i.jsx)(n.code,{children:"personal"})," namespace, you might use a name such as ",(0,i.jsx)(n.code,{children:"personal:user#42"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Furthermore, it's possible to specify multiple user IDs in the channel name, separated by a comma: ",(0,i.jsx)(n.code,{children:"dialog#42,43"}),". In this case, only users with IDs ",(0,i.jsx)(n.code,{children:"42"})," and ",(0,i.jsx)(n.code,{children:"43"})," are permitted to subscribe to this channel."]}),"\n",(0,i.jsx)(n.p,{children:"This setup is ideal for channels that have a static list of allowed users, such as channels for personal messages to a single user or dialogue channels between specific users. However, for dynamic access management of a channel for numerous users, this type of channel is not appropriate."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["User-limited channels must be enabled for a channel namespace using ",(0,i.jsx)(n.a,{href:"#allow_user_limited_channels",children:"allow_user_limited_channels"})," option. See below more information about channel options and channel namespaces."]})}),"\n",(0,i.jsxs)(n.h3,{id:"private-channel-prefix-",children:["private channel prefix (",(0,i.jsx)(n.code,{children:"$"}),")"]}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo maintains compatibility with its previous versions which had concept of private channels. In earlier versions \u2014 specifically Centrifugo v1, v2, and v3\u2014only \u2013 only channels beginning with ",(0,i.jsx)(n.code,{children:"$"})," required a subscription JWT for subscribing. With Centrifugo v4, this is no longer the case; clients can subscribe to any channel if they have a valid subscription token."]}),"\n",(0,i.jsxs)(n.p,{children:["However, for namespaces where the ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," option is activated, Centrifugo prohibits subscriptions to channels that start with the ",(0,i.jsx)(n.code,{children:"private_channel_prefix"})," (which defaults to ",(0,i.jsx)(n.code,{children:"$"}),") unless a subscription token is provided. This restriction is designed to facilitate a secure migration to Centrifugo v4 or later versions."]}),"\n",(0,i.jsx)(n.h3,{id:"channel-is-just-a-string",children:"Channel is just a string"}),"\n",(0,i.jsxs)(n.p,{children:["Remember that a channel is uniquely identified by its string name. Do not assume that ",(0,i.jsx)(n.code,{children:"$news"})," and ",(0,i.jsx)(n.code,{children:"news"})," are the same; they are different because their names are not identical. Therefore, if a user is subscribed to ",(0,i.jsx)(n.code,{children:"$news"}),", they will not receive messages published to ",(0,i.jsx)(n.code,{children:"news"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["The channels ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," and ",(0,i.jsx)(n.code,{children:"dialog#43,42"})," are considered different as well. Centrifugo only applies permission checks when a user subscribes to a channel. So if user-limited channels are enabled then the user with ID ",(0,i.jsx)(n.code,{children:"42"})," will be able to subscribe on both ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," and ",(0,i.jsx)(n.code,{children:"dialog#43,42"}),". But Centrifugo does no magic regarding channel strings when keeping channel->to->subscribers map. So if the user subscribed on ",(0,i.jsx)(n.code,{children:"dialog#42,43"})," you must publish messages to exactly that channel: ",(0,i.jsx)(n.code,{children:"dialog#42,43"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["The same reasoning applies to channels within namespaces. Channels ",(0,i.jsx)(n.code,{children:"chat:index"})," and ",(0,i.jsx)(n.code,{children:"index"})," are not the same \u2014 they are distinct and, moreover, they belong to different namespaces. The concept of channel namespaces in Centrifugo will be discussed shortly."]}),"\n",(0,i.jsx)(n.h2,{id:"channel-namespaces",children:"Channel namespaces"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo allows configuring a list of channel namespaces. Namespaces are optional but super-useful."}),"\n",(0,i.jsxs)(n.p,{children:["A namespace is a container for options applied to channels that start with the namespace name + ",(0,i.jsx)(n.code,{children:":"})," separator. For example, if you define a namespace named ",(0,i.jsx)(n.code,{children:"personal"})," in the configuration, all channels starting with ",(0,i.jsx)(n.code,{children:"personal:"})," (such as ",(0,i.jsx)(n.code,{children:"personal:1"})," or ",(0,i.jsx)(n.code,{children:"personal:2"}),") will inherit the options defined for the ",(0,i.jsx)(n.code,{children:"personal"})," namespace. This gives you great control over channel behavior, allowing you to set different options for various real-time features in your application."]}),"\n",(0,i.jsxs)(n.p,{children:["Namespace has a name, and can contain all the ",(0,i.jsx)(n.a,{href:"#channel-options",children:"channel options"}),". Namespace ",(0,i.jsx)(n.code,{children:"name"})," is required to be set. Name of namespace must be unique, must consist of letters, numbers, underscores, or hyphens and be more than 2 symbols length i.e. satisfy regexp ",(0,i.jsx)(n.code,{children:"^[-a-zA-Z0-9_]{2,}$"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When you want to use specific namespace options your channel must be prefixed with namespace name and ",(0,i.jsx)(n.code,{children:":"})," separator: ",(0,i.jsx)(n.code,{children:"public:messages"}),", ",(0,i.jsx)(n.code,{children:"gossips:messages"})," are two channels in ",(0,i.jsx)(n.code,{children:"public"})," and ",(0,i.jsx)(n.code,{children:"gossips"})," namespaces."]}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo looks for ",(0,i.jsx)(n.code,{children:":"})," symbol in the channel name, if found \u2013 extracts the namespace name, and applies all the configured namespace channel options while processing protocol commands from a client or server API calls."]}),"\n",(0,i.jsxs)(n.p,{children:["All things together here is an example of ",(0,i.jsx)(n.code,{children:"config.json"})," which includes some top-level channel options set and has 2 additional channel namespaces configured:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "very-long-secret-key",\n "api_key": "secret-api-key",\n \n "presence": true,\n "history_size": 10,\n "history_ttl": "30s",\n \n "namespaces": [\n {\n "name": "facts",\n "history_size": 10,\n "history_ttl": "300s"\n },\n {\n "name": "gossips"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"news"})," will use globally defined channel options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"facts:sport"})," will use ",(0,i.jsx)(n.code,{children:"facts"})," namespace options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"gossips:sport"})," will use ",(0,i.jsx)(n.code,{children:"gossips"})," namespace options."]}),"\n",(0,i.jsxs)(n.li,{children:["Channel ",(0,i.jsx)(n.code,{children:"xxx:hello"})," will result into subscription error since there is no ",(0,i.jsx)(n.code,{children:"xxx"})," namespace defined in the configuration above."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Channel namespaces also work with private channels and user-limited channels"}),". For example, if you have a namespace called ",(0,i.jsx)(n.code,{children:"dialogs"})," then the private channel can be constructed as ",(0,i.jsx)(n.code,{children:"$dialogs:gossips"}),", user-limited channel can be constructed as ",(0,i.jsx)(n.code,{children:"dialogs:dialog#1,2"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["There is ",(0,i.jsx)(n.strong,{children:"no inheritance"})," in channel options and namespaces \u2013 for example, you defined ",(0,i.jsx)(n.code,{children:"presence: true"})," on a top level of configuration and then defined a namespace \u2013 that namespace won't have online presence enabled - you must enable it for a namespace explicitly."]})}),"\n",(0,i.jsx)(n.p,{children:"There are many options which can be set for channel namespace (on top-level and to named one) to modify behavior of channels belonging to a namespace. Below we describe all these options."}),"\n",(0,i.jsx)(n.h2,{id:"channel-options",children:"Channel options"}),"\n",(0,i.jsx)(n.p,{children:"Channel behavior can be modified by using channel options. Channel options can be defined on configuration top-level and for every namespace."}),"\n",(0,i.jsx)(n.h3,{id:"presence",children:"presence"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"presence"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 enable/disable online presence information for channels in a namespace."]}),"\n",(0,i.jsx)(n.p,{children:"Online presence is information about clients currently subscribed to the channel. It contains each subscriber's client ID, user ID, connection info, and channel info. By default, this option is off so no presence information will be available for channels."}),"\n",(0,i.jsxs)(n.p,{children:["Let's say you have a channel ",(0,i.jsx)(n.code,{children:"chat:index"})," with two users subscribed (IDs ",(0,i.jsx)(n.code,{children:"2694"})," and ",(0,i.jsx)(n.code,{children:"56"}),"). User ",(0,i.jsx)(n.code,{children:"56"})," has one connection to Centrifugo. User ",(0,i.jsx)(n.code,{children:"2694"})," has two connections to Centrifugo from different browser tabs. The presence data might look like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: " \\\n --request POST \\\n --data \'{"channel": "chat:index"}\' \\\n http://localhost:8000/api/presence\n{\n "result": {\n "presence": {\n "66fdf8d1-06f0-4375-9fac-db959d6ee8d6": {\n "user": "2694",\n "client": "66fdf8d1-06f0-4375-9fac-db959d6ee8d6",\n "conn_info": {"name": "Alex"}\n },\n "d4516dd3-0b6e-4cfe-84e8-0342fd2bb20c": {\n "user": "2694",\n "client": "d4516dd3-0b6e-4cfe-84e8-0342fd2bb20c",\n "conn_info": {"name": "Alex"}\n }\n "g3216dd3-1b6e-tcfe-14e8-1342fd2bb20c": {\n "user": "56",\n "client": "g3216dd3-1b6e-tcfe-14e8-1342fd2bb20c",\n "conn_info": {"name": "Alice"}\n }\n }\n }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To call presence API from the client connection side client must have permission to do so. See ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#presence-permission-model",children:"presence permission model"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Enabling channel online presence adds some overhead since Centrifugo needs to maintain an additional data structure (in a process memory or in a broker memory/disk). So only use it for channels where presence is required."})}),"\n",(0,i.jsxs)(n.p,{children:["See more details about ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#online-presence-considerations",children:"online presence design"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"join_leave",children:"join_leave"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"join_leave"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 enable/disable sending join and leave messages when the client subscribes to a channel (unsubscribes from a channel). Join/leave event includes information about the connection that triggered an event \u2013 client ID, user ID, connection info, and channel info (similar to entry inside presence information)."]}),"\n",(0,i.jsxs)(n.p,{children:["Enabling ",(0,i.jsx)(n.code,{children:"join_leave"})," means that Join/Leave messages will start being emitted, but by default they are not delivered to clients subscribed to a channel. You need to force this using namespace option ",(0,i.jsx)(n.a,{href:"#forcepushjoinleave",children:"force_push_join_leave"})," or explicitly provide intent from a client-side (in this case client must have permission to call presence API)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:'Keep in mind that join/leave messages can generate a huge number of messages in a system if turned on for channels with a large number of active subscribers. If you have channels with a large number of subscribers consider avoiding using this feature. It\'s hard to say what is "large" for you though \u2013 just estimate the load based on the fact that each subscribe/unsubscribe event in a channel with N subscribers will result into N messages broadcasted to all. If all clients reconnect at the same time the amount of generated messages is N^2.'})}),"\n",(0,i.jsx)(n.p,{children:"Join/leave messages distributed only with at most once delivery guarantee."}),"\n",(0,i.jsx)(n.h3,{id:"force_push_join_leave",children:"force_push_join_leave"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"When on all clients will receive join/leave events for a channel in a namespace automatically \u2013 without explicit intent to consume join/leave messages from the client side."}),"\n",(0,i.jsx)(n.p,{children:"If pushing join/leave is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel presence (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.h3,{id:"history_size",children:"history_size"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_size"})," (integer, default ",(0,i.jsx)(n.code,{children:"0"}),") \u2013 history size (amount of messages) for channels. As Centrifugo keeps all history messages in process memory (or in a broker memory) it's very important to limit the maximum amount of messages in channel history with a reasonable value. ",(0,i.jsx)(n.code,{children:"history_size"})," defines the maximum amount of messages that Centrifugo will keep for ",(0,i.jsx)(n.strong,{children:"each"})," channel in the namespace. As soon as history has more messages than defined by history size \u2013 old messages will be evicted."]}),"\n",(0,i.jsxs)(n.p,{children:["Setting only ",(0,i.jsx)(n.code,{children:"history_size"})," ",(0,i.jsx)(n.strong,{children:"is not enough to enable history in channels"})," \u2013 you also need to wisely configure ",(0,i.jsx)(n.code,{children:"history_ttl"})," option (see below)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Enabling channel history adds some overhead (both memory and CPU) since Centrifugo needs to maintain an additional data structure (in a process memory or a broker memory/disk). So only use history for channels where it's required."})}),"\n",(0,i.jsx)(n.h3,{id:"history_ttl",children:"history_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),", default ",(0,i.jsx)(n.code,{children:"0s"}),") \u2013 interval how long to keep channel history messages (with seconds precision)."]}),"\n",(0,i.jsx)(n.p,{children:"As all history is storing in process memory (or in a broker memory) it is also very important to get rid of old history data for unused (inactive for a long time) channels."}),"\n",(0,i.jsx)(n.p,{children:"By default history TTL duration is zero \u2013 this means that channel history is disabled."}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsxs)(n.strong,{children:["Again \u2013 to turn on history you should wisely configure both ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," options"]}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Also note, that ",(0,i.jsx)(n.code,{children:"history_ttl"})," must be less than ",(0,i.jsx)(n.a,{href:"#history_meta_ttl",children:"history_meta_ttl"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"For example for top-level channels (which do not belong to a namespace):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "history_size": 10,\n "history_ttl": "60s"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Here's an example. You enabled history for the ",(0,i.jsx)(n.code,{children:"chat"})," namespace and sent two messages in the ",(0,i.jsx)(n.code,{children:"chat:index"})," channel. The history will look like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: " \\\n --request POST \\\n --data \'{"channel": "chat:index", "limit": 100}\' \\\n http://localhost:8000/api/history\n{\n "result": {\n "publications": [\n {\n "data": {\n "input": "1"\n },\n "offset": 1\n },\n {\n "data": {\n "input": "2"\n },\n "offset": 2\n }\n ],\n "epoch": "gWuY",\n "offset": 2\n }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To call the history API from the client side, the client must have the necessary permissions. For more details, see the ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#history-permission-model",children:"history permission model"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["See additional information about offsets and epoch in ",(0,i.jsx)(n.a,{href:"/docs/server/history_and_recovery",children:"History and recovery"})," chapter."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"The persistence properties of history data depend on the Centrifugo engine in use. For instance, with the Memory engine (default), history is retained only until the Centrifugo node restarts. In contrast, with the Redis engine, persistence is determined by the Redis server's configuration (similarly for Redis-compatible storages and Tarantool)."})}),"\n",(0,i.jsx)(n.h3,{id:"history_meta_ttl",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),") \u2013 sets a time of history stream metadata expiration (with seconds precision)."]}),"\n",(0,i.jsxs)(n.p,{children:["If not specified Centrifugo namespace inherits value from ",(0,i.jsx)(n.code,{children:"global_history_meta_ttl"})," (",(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"duration"}),") option which is 30 days by default (",(0,i.jsx)(n.code,{children:'"720h"'}),"). This should be a good default for most use cases to avoid tweaking ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," on a namespace level at all. If you have ",(0,i.jsx)(n.code,{children:"history_ttl"})," greater than 30 days \u2013 then increase ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," for namespace (recommended) or increase ",(0,i.jsx)(n.code,{children:"global_history_meta_ttl"})," to be larger than ",(0,i.jsx)(n.code,{children:"history_ttl"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"The motivation to have history meta information TTL is as follows. When using a history in a channel, Centrifugo keeps some metadata for each channel stream. Metadata includes the latest stream offset and its epoch value. In some cases, when channels are created for \u0430 short time and then not used anymore, created metadata can stay in memory while not useful. For example, you can have a personal user channel but after using your app for a while user left it forever. From a long-term perspective, this can be an unwanted memory growth. Setting a reasonable value to this option can help to expire metadata faster (or slower if you need it). The rule of thumb here is to keep this value larger than history TTL used."}),"\n",(0,i.jsx)(n.h3,{id:"force_positioning",children:"force_positioning"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_positioning"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when the ",(0,i.jsx)(n.code,{children:"force_positioning"})," option is on Centrifugo forces all subscriptions in a namespace to be ",(0,i.jsx)(n.code,{children:"positioned"}),". I.e. Centrifugo will try to compensate at most once delivery of PUB/SUB broker checking client position inside a stream."]}),"\n",(0,i.jsxs)(n.p,{children:["If Centrifugo detects a bad position of the client (i.e. potential message loss) it disconnects a client with the ",(0,i.jsx)(n.code,{children:"Insufficient state"})," disconnect code. Also, when the position option is enabled Centrifugo exposes the current stream top ",(0,i.jsx)(n.code,{children:"offset"})," and current ",(0,i.jsx)(n.code,{children:"epoch"})," in subscribe reply making it possible for a client to manually recover its state upon disconnect using history API."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_positioning"})," option must be used in conjunction with reasonably configured message history for a channel i.e. ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," ",(0,i.jsx)(n.strong,{children:"must be set"})," (because Centrifugo uses channel history to check client position in a stream)."]}),"\n",(0,i.jsx)(n.p,{children:"If positioning is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel history (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.h3,{id:"force_recovery",children:"force_recovery"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when the ",(0,i.jsx)(n.code,{children:"force_recovery"})," option is on Centrifugo forces all subscriptions in a namespace to be ",(0,i.jsx)(n.code,{children:"recoverable"}),". When enabled Centrifugo will try to recover missed publications in channels after a client reconnects for some reason (bad internet connection for example). Also when the recovery feature is on Centrifugo automatically enables properties of the ",(0,i.jsx)(n.code,{children:"force_positioning"})," option described above."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery"})," option must be used in conjunction with reasonably configured message history for channel i.e. ",(0,i.jsx)(n.code,{children:"history_size"})," and ",(0,i.jsx)(n.code,{children:"history_ttl"})," ",(0,i.jsx)(n.strong,{children:"must be set"})," (because Centrifugo uses channel history to recover messages)."]}),"\n",(0,i.jsx)(n.p,{children:"If recovery is not forced then client can provide a corresponding Subscription option to enable it \u2013 but it should have permissions to access channel history (by having an explicit capability or if allowed on a namespace level)."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Not all real-time events require this feature turned on so think wisely when you need this. When this option is turned on your application should be designed in a way to tolerate duplicate messages coming from a channel (currently Centrifugo returns recovered publications in order and without duplicates but this is an implementation detail that can be theoretically changed in the future). See more details about how recovery works in ",(0,i.jsx)(n.a,{href:"/docs/server/history_and_recovery",children:"special chapter"}),"."]})}),"\n",(0,i.jsx)(n.h3,{id:"force_recovery_mode",children:"force_recovery_mode"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"force_recovery_mode"})," (string, possible values are ",(0,i.jsx)(n.code,{children:"stream"})," or ",(0,i.jsx)(n.code,{children:"cache"}),", when not specified Centrifugo uses ",(0,i.jsx)(n.code,{children:'"stream"'}),"). Allows setting recovery mode for all connections which use recovery in the namespace. By default, Centrifugo uses ",(0,i.jsx)(n.code,{children:"stream"})," recovery mode \u2013 a mode where subscriber interested in all messages to be delivered. The alternative recovery mode which may be forced by using this option is ",(0,i.jsx)(n.code,{children:"cache"})," \u2013 see the detailed description in ",(0,i.jsx)(n.a,{href:"/docs/server/cache_recovery",children:"Cache recovery mode"})," chapter."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_subscribe_for_client",children:"allow_subscribe_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when on all non-anonymous clients will be able to subscribe to any channel in a namespace. To additionally allow anonymous users to subscribe turn on ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_anonymous"})," (see below)."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Turning this option on effectively makes namespace public \u2013 no subscribe permissions will be checked (only the check that current connection is authenticated - i.e. has non-empty user ID). Make sure this is really what you want in terms of channels security."})}),"\n",(0,i.jsx)(n.h3,{id:"allow_subscribe_for_anonymous",children:"allow_subscribe_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_subscribe_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients (with empty user ID) should be able to subscribe on channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_subscriber",children:"allow_publish_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - when the ",(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," option is enabled client can publish into a channel in namespace directly from the client side over real-time connection but only if client subscribed to that channel."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Keep in mind that in this case subscriber can publish any payload to a channel \u2013 Centrifugo does not validate input at all. Your app backend won't receive those messages - publications just go through Centrifugo towards channel subscribers. Consider always validate messages which are being published to channels (i.e. using server API to publish after validating input on the backend side, or using ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#publish-proxy",children:"publish proxy"})," - see ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#idiomatic-usage",children:"idiomatic usage"}),")."]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," (or ",(0,i.jsx)(n.code,{children:"allow_publish_for_client"})," mentioned below) option still can be useful to send something without backend-side validation and saving it into a database \u2013 for example, this option may be handy for demos and quick prototyping real-time app ideas."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_client",children:"allow_publish_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 when on allows clients to publish messages into channels directly (from a client-side). It's like ",(0,i.jsx)(n.code,{children:"allow_publish_for_subscriber"})," \u2013 but client should not be a channel subscriber to publish."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Keep in mind that in this case client can publish any payload to a channel \u2013 Centrifugo does not validate input at all. Your app backend won't receive those messages - publications just go through Centrifugo towards channel subscribers. Consider always validate messages which are being published to channels (i.e. using server API to publish after validating input on the backend side, or using ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#publish-proxy",children:"publish proxy"})," - see ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#idiomatic-usage",children:"idiomatic usage"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"allow_publish_for_anonymous",children:"allow_publish_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_publish_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to publish into channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_subscriber",children:"allow_history_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows clients who subscribed on a channel to call history API from that channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_client",children:"allow_history_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows all clients to call history information in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_history_for_anonymous",children:"allow_history_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_history_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to call history from channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_subscriber",children:"allow_presence_for_subscriber"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_subscriber"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows clients who subscribed on a channel to call presence information from that channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_client",children:"allow_presence_for_client"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_client"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 allows all clients to call presence information in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_presence_for_anonymous",children:"allow_presence_for_anonymous"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_presence_for_anonymous"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turn on if anonymous clients should be able to call presence from channels in a namespace."]}),"\n",(0,i.jsx)(n.h3,{id:"allow_user_limited_channels",children:"allow_user_limited_channels"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allow_user_limited_channels"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - allows using user-limited channels in a namespace for checking subscribe permission."]}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["If client subscribes to a user-limited channel while this option is off then server rejects subscription with ",(0,i.jsx)(n.code,{children:"103: permission denied"})," error."]})}),"\n",(0,i.jsx)(n.h3,{id:"channel_regex",children:"channel_regex"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"channel_regex"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 is an option to set a regular expression for channels allowed in the namespace. By default Centrifugo does not limit channel name variations. For example, if you have a namespace ",(0,i.jsx)(n.code,{children:"chat"}),", then channel names inside this namespace are not really limited, it can be ",(0,i.jsx)(n.code,{children:"chat:index"}),", ",(0,i.jsx)(n.code,{children:"chat:1"}),", ",(0,i.jsx)(n.code,{children:"chat:2"}),", ",(0,i.jsx)(n.code,{children:"chat:zzz"})," and so on. But if you want to be strict and know possible channel patterns you can use ",(0,i.jsx)(n.code,{children:"channel_regex"})," option. This is especially useful in namespaces where all clients can subscribe to channels."]}),"\n",(0,i.jsxs)(n.p,{children:["For example, let's only allow digits after ",(0,i.jsx)(n.code,{children:"chat:"})," for channel names in a ",(0,i.jsx)(n.code,{children:"chat"})," namespace:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "chat",\n "allow_subscribe_for_client": true,\n "channel_regex": "^[\\d+]$"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Note, that we are skipping ",(0,i.jsx)(n.code,{children:"chat:"})," part in regex. Since namespace prefix is the same for all channels in a namespace we only match the rest (after the prefix) of channel name."]})}),"\n",(0,i.jsx)(n.p,{children:"Channel regex only checked for client-side subscriptions, if you are using server-side subscriptions Centrifugo won't check the regex."}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo uses Go language ",(0,i.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"regexp"})," package for regular expressions."]}),"\n",(0,i.jsx)(n.h3,{id:"delta_publish",children:"delta_publish"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"delta_publish"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") allows marking all publications in the namespace with ",(0,i.jsx)(n.code,{children:"delta"})," flag, i.e. all publications will result into delta updates for subscribers which negotiated delta compression for a channel."]}),"\n",(0,i.jsx)(n.h3,{id:"allowed_delta_types",children:"allowed_delta_types"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"allowed_delta_types"})," (array of strings, the only allowed value now is ",(0,i.jsx)(n.code,{children:"fossil"}),") - provide an array of allowed delta compression types in the namespace. If not specified \u2013 client won't be able to negotiate delta compression in channels."]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_subscribe",children:"proxy_subscribe"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_subscribe"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on subscribe proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_publish",children:"proxy_publish"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_publish"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on publish proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_sub_refresh",children:"proxy_sub_refresh"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_sub_refresh"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") \u2013 turns on sub refresh proxy, more info in ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy chapter"})]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_subscribe_stream",children:"proxy_subscribe_stream"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_subscribe_stream"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),") - turns on subscribe stream proxy, see ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams",children:"subscription streams"})]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_proxy_name",children:"subscribe_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on subscribe proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_subscribe"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"publish_proxy_name",children:"publish_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"publish_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on publish proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_publish"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"sub_refresh_proxy_name",children:"sub_refresh_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"sub_refresh_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on sub refresh proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_sub_refresh"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_stream_proxy_name",children:"subscribe_stream_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_stream_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") \u2013 turns on subscribe stream proxy when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_subscribe_stream"})," option defined above is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"cache_empty_proxy_name",children:"cache_empty_proxy_name"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"cache_empty_proxy_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 turns on ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_cache_empty",children:"cache empty proxy"})," when ",(0,i.jsx)(n.a,{href:"/docs/server/proxy_streams#granular-proxy-mode",children:"granular proxy mode"})," is used. Note that ",(0,i.jsx)(n.code,{children:"proxy_cache_empty"})," option is ignored in granular proxy mode."]}),"\n",(0,i.jsx)(n.h3,{id:"proxy_cache_empty",children:"proxy_cache_empty"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"proxy_cache_empty"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),", Centrifugo PRO only) - turns on cache empty proxy, see ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_cache_empty",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"shared_position_sync",children:"shared_position_sync"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"shared_position_sync"})," (boolean, default ",(0,i.jsx)(n.code,{children:"false"}),", Centrifugo PRO only) - can help reducing the number of position synchronization requests from Centrifugo to Broker's history API, see ",(0,i.jsx)(n.a,{href:"/docs/pro/engine_optimizations#shared-position-sync",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"channel_state_events",children:"channel_state_events"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"channel_state_events"})," (array of strings, empty by default, Centrifugo PRO only) - can help configuring notifications about channel's ",(0,i.jsx)(n.code,{children:"occupied"})," and ",(0,i.jsx)(n.code,{children:"vacated"})," state. See ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_state_events",children:"more details"})," in Centrifugo PRO docs."]}),"\n",(0,i.jsx)(n.h3,{id:"subscribe_cel",children:"subscribe_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"subscribe_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for subscribe permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"publish_cel",children:"publish_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"publish_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for publish permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"history_cel",children:"history_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for history permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h3,{id:"presence_cel",children:"presence_cel"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"presence_cel"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),", Centrifugo PRO only) \u2013 CEL expression for presence permission, see more details in ",(0,i.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," of Centrifugo PRO."]}),"\n",(0,i.jsx)(n.h2,{id:"channel-config-examples",children:"Channel config examples"}),"\n",(0,i.jsx)(n.p,{children:"Let's look at how to set some of these options in a config. In this example we turning on presence, history features, forcing publication recovery. Also allowing all client connections (including anonymous users) to subscribe to channels and call publish, history, presence APIs if subscribed."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "my-secret-key",\n "api_key": "secret-api-key",\n "presence": true,\n "history_size": 10,\n "history_ttl": "300s",\n "force_recovery": true,\n "allow_subscribe_for_client": true,\n "allow_subscribe_for_anonymous": true,\n "allow_publish_for_subscriber": true,\n "allow_publish_for_anonymous": true,\n "allow_history_for_subscriber": true,\n "allow_history_for_anonymous": true,\n "allow_presence_for_subscriber": true,\n "allow_presence_for_anonymous": true\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Here we set channel options on config top-level \u2013 these options will affect channels without namespace. In many cases defining namespaces is a recommended approach so you can manage options for every real-time feature separately. With namespaces the above config may transform to:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "my-secret-key",\n "api_key": "secret-api-key",\n "namespaces": [\n {\n "name": "feed",\n "presence": true,\n "history_size": 10,\n "history_ttl": "300s",\n "force_recovery": true,\n "allow_subscribe_for_client": true,\n "allow_subscribe_for_anonymous": true,\n "allow_publish_for_subscriber": true,\n "allow_publish_for_anonymous": true,\n "allow_history_for_subscriber": true,\n "allow_history_for_anonymous": true,\n "allow_presence_for_subscriber": true,\n "allow_presence_for_anonymous": true\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["In this case channels should be prefixed with ",(0,i.jsx)(n.code,{children:"feed:"})," to follow the behavior configured for a ",(0,i.jsx)(n.code,{children:"feed"})," namespace."]})]})}function d(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},88550:(e,n,s)=>{s.d(n,{Z:()=>i});const i=s.p+"assets/images/pub_sub-5477abf6fb38219fc0848d9c9c3dc2b1.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>l,a:()=>a});var i=s(67294);const o={},r=i.createContext(o);function a(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/09382599.d2d33efd.js b/assets/js/09382599.503acccd.js similarity index 97% rename from assets/js/09382599.d2d33efd.js rename to assets/js/09382599.503acccd.js index b9622e1d0..9d2a16a79 100644 --- a/assets/js/09382599.d2d33efd.js +++ b/assets/js/09382599.503acccd.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8951],{73857:(c,e,v)=>{v.r(e),v.d(e,{default:()=>s});v(67294);var l=v(85893);function s(){return(0,l.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 140 40",children:(0,l.jsx)("path",{fill:"#000000",d:"M18.412 12.216c-4.287 0-5.808 3.454-5.912 3.707a6.332 6.332 0 0 0-5.91-3.707 6.627 6.627 0 0 0-4.653 2.01A6.856 6.856 0 0 0 0 18.975c0 7.282 6.87 13.184 12.5 13.184 5.628 0 12.5-5.902 12.5-13.184a6.856 6.856 0 0 0-1.937-4.748 6.627 6.627 0 0 0-4.65-2.01zm1.13 7.037c0 1.9-0.742 3.722-2.063 5.065a6.983 6.983 0 0 1-4.98 2.099 6.983 6.983 0 0 1-4.98-2.099 7.226 7.226 0 0 1-2.062-5.065v-0.205h2.49v0.205A4.674 4.674 0 0 0 9.28 22.53a4.517 4.517 0 0 0 3.221 1.357 4.517 4.517 0 0 0 3.221-1.357 4.675 4.675 0 0 0 1.335-3.277v-0.205h2.49l-0.006 0.205zm110.438-7.03c-5.791 0-10.02 4.252-10.02 9.988 0 5.735 4.232 9.989 10.02 9.989 5.788 0 10.02-4.252 10.02-9.99 0-5.836-4.23-9.986-10.02-9.986zm0 15.93c-3.174 0-5.338-2.458-5.338-5.942 0-3.485 2.164-5.94 5.338-5.94s5.337 2.458 5.337 5.94c0 3.481-2.166 5.943-5.337 5.943zm-22.251-15.93c-5.79 0-10.02 4.252-10.02 9.988 0 5.735 4.232 9.989 10.02 9.989 5.788 0 10.023-4.252 10.023-9.99-0.008-5.836-4.237-9.986-10.023-9.986zm0 15.93c-3.172 0-5.337-2.458-5.337-5.942 0-3.485 2.165-5.94 5.337-5.94 3.172 0 5.337 2.458 5.337 5.94 0 3.481-2.17 5.943-5.337 5.943zm-65.012-15.93c-3.122 0-5.44 1.383-6.799 3.74V7h-4.232v14.7c0 6.248 3.625 10.5 9.868 10.5 5.136 0 9.92-3.74 9.92-10.09-0.007-5.379-3.168-9.886-8.757-9.886zm-1.208 15.725c-3.423 0-5.387-2.509-5.387-5.737 0-3.277 1.912-5.94 5.337-5.94 3.274 0 5.338 2.663 5.338 5.94-0.002 3.228-1.865 5.738-5.288 5.738zm22.104-15.724c-5.136 0-9.92 3.74-9.92 10.09 0 5.379 3.171 9.876 8.763 9.876 3.121 0 5.437-1.383 6.796-3.74v3.545h4.232v-9.27c-0.002-6.25-3.627-10.501-9.87-10.501zm0 15.93c-3.373 0-5.228-2.563-5.228-5.84 0-3.226 1.865-5.839 5.288-5.839s5.387 2.51 5.387 5.736c-0.01 3.279-2.024 5.943-5.447 5.943zM91.256 7v8.964c-1.359-2.358-3.674-3.74-6.796-3.74-5.591 0-8.763 4.507-8.763 9.875 0 6.455 4.481 10.09 9.709 10.09 6.244 0 10.07-4.25 10.07-10.5V7.003L91.256 7zm-5.589 20.948c-3.423 0-5.287-2.509-5.287-5.737 0-3.277 2.066-5.94 5.337-5.94 3.426 0 5.338 2.612 5.338 5.94-0.005 3.228-1.965 5.738-5.388 5.738z"})})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8951],{63349:(c,e,v)=>{v.r(e),v.d(e,{default:()=>s});v(67294);var l=v(85893);function s(){return(0,l.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 140 40",children:(0,l.jsx)("path",{fill:"#000000",d:"M18.412 12.216c-4.287 0-5.808 3.454-5.912 3.707a6.332 6.332 0 0 0-5.91-3.707 6.627 6.627 0 0 0-4.653 2.01A6.856 6.856 0 0 0 0 18.975c0 7.282 6.87 13.184 12.5 13.184 5.628 0 12.5-5.902 12.5-13.184a6.856 6.856 0 0 0-1.937-4.748 6.627 6.627 0 0 0-4.65-2.01zm1.13 7.037c0 1.9-0.742 3.722-2.063 5.065a6.983 6.983 0 0 1-4.98 2.099 6.983 6.983 0 0 1-4.98-2.099 7.226 7.226 0 0 1-2.062-5.065v-0.205h2.49v0.205A4.674 4.674 0 0 0 9.28 22.53a4.517 4.517 0 0 0 3.221 1.357 4.517 4.517 0 0 0 3.221-1.357 4.675 4.675 0 0 0 1.335-3.277v-0.205h2.49l-0.006 0.205zm110.438-7.03c-5.791 0-10.02 4.252-10.02 9.988 0 5.735 4.232 9.989 10.02 9.989 5.788 0 10.02-4.252 10.02-9.99 0-5.836-4.23-9.986-10.02-9.986zm0 15.93c-3.174 0-5.338-2.458-5.338-5.942 0-3.485 2.164-5.94 5.338-5.94s5.337 2.458 5.337 5.94c0 3.481-2.166 5.943-5.337 5.943zm-22.251-15.93c-5.79 0-10.02 4.252-10.02 9.988 0 5.735 4.232 9.989 10.02 9.989 5.788 0 10.023-4.252 10.023-9.99-0.008-5.836-4.237-9.986-10.023-9.986zm0 15.93c-3.172 0-5.337-2.458-5.337-5.942 0-3.485 2.165-5.94 5.337-5.94 3.172 0 5.337 2.458 5.337 5.94 0 3.481-2.17 5.943-5.337 5.943zm-65.012-15.93c-3.122 0-5.44 1.383-6.799 3.74V7h-4.232v14.7c0 6.248 3.625 10.5 9.868 10.5 5.136 0 9.92-3.74 9.92-10.09-0.007-5.379-3.168-9.886-8.757-9.886zm-1.208 15.725c-3.423 0-5.387-2.509-5.387-5.737 0-3.277 1.912-5.94 5.337-5.94 3.274 0 5.338 2.663 5.338 5.94-0.002 3.228-1.865 5.738-5.288 5.738zm22.104-15.724c-5.136 0-9.92 3.74-9.92 10.09 0 5.379 3.171 9.876 8.763 9.876 3.121 0 5.437-1.383 6.796-3.74v3.545h4.232v-9.27c-0.002-6.25-3.627-10.501-9.87-10.501zm0 15.93c-3.373 0-5.228-2.563-5.228-5.84 0-3.226 1.865-5.839 5.288-5.839s5.387 2.51 5.387 5.736c-0.01 3.279-2.024 5.943-5.447 5.943zM91.256 7v8.964c-1.359-2.358-3.674-3.74-6.796-3.74-5.591 0-8.763 4.507-8.763 9.875 0 6.455 4.481 10.09 9.709 10.09 6.244 0 10.07-4.25 10.07-10.5V7.003L91.256 7zm-5.589 20.948c-3.423 0-5.287-2.509-5.287-5.737 0-3.277 2.066-5.94 5.337-5.94 3.426 0 5.338 2.612 5.338 5.94-0.005 3.228-1.965 5.738-5.388 5.738z"})})}}}]); \ No newline at end of file diff --git a/assets/js/0d503bfe.a27685cc.js b/assets/js/0d503bfe.7ffec748.js similarity index 97% rename from assets/js/0d503bfe.a27685cc.js rename to assets/js/0d503bfe.7ffec748.js index 5083843f7..4e8e31818 100644 --- a/assets/js/0d503bfe.a27685cc.js +++ b/assets/js/0d503bfe.7ffec748.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7401],{82923:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>t,default:()=>u,frontMatter:()=>r,metadata:()=>c,toc:()=>d});var o=i(85893),s=i(11151);const r={id:"admin_idp_auth",sidebar_label:"SSO for admin UI (OIDC)",title:"SSO for admin UI using OpenID connect (OIDC)"},t=void 0,c={id:"pro/admin_idp_auth",title:"SSO for admin UI using OpenID connect (OIDC)",description:"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",source:"@site/docs/pro/admin_idp_auth.md",sourceDirName:"pro",slug:"/pro/admin_idp_auth",permalink:"/docs/pro/admin_idp_auth",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/admin_idp_auth.md",tags:[],version:"current",frontMatter:{id:"admin_idp_auth",sidebar_label:"SSO for admin UI (OIDC)",title:"SSO for admin UI using OpenID connect (OIDC)"},sidebar:"Pro",previous:{title:"Push notification API",permalink:"/docs/pro/push_notifications"},next:{title:"User status API",permalink:"/docs/pro/user_status"}},a={},d=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["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 ",(0,o.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Identity_provider",children:"Identity Providers"})," (IDP), such as Okta, KeyCloak, Google Workspace, Azure and others. Most of the modern providers which support ",(0,o.jsx)(n.a,{href:"https://openid.net/specs/openid-connect-core-1_0.html",children:"OpenID connect"})," (OIDC) protocol with ",(0,o.jsx)(n.a,{href:"https://oauth.net/2/pkce/",children:"Proof Key for Code Exchange"}),"\n(PKCE) and ",(0,o.jsx)(n.a,{href:"https://openid.net/specs/openid-connect-discovery-1_0.html",children:"OpenID Connect Discovery"})," are supported. This provides a way to integrate Centrifugo PRO into your existing ",(0,o.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Single_sign-on",children:"Single Sign-On"})," (SSO) infrastructure."]}),"\n",(0,o.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,o.jsx)(n.p,{children:"As soon as OIDC integration configured, instead of password field Centrifugo PRO admin web UI shows a button to log in using a configured Identity Provider. As soon as user successfully logs in over the IDP, user is redirected back to Centrifugo admin UI. Centrifugo checks user's access token and permissions to access admin functionality upon every request to admin resources."}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.img,{src:i(96589).Z+"",width:"1972",height:"1012"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "admin_oidc": {\n "enabled": true,\n "display_name": "Keycloak",\n "issuer": "http://localhost:8080/realms/master",\n "client_id": "myclient",\n "audience": "myclient",\n "redirect_uri": "http://localhost:8000",\n "extra_scopes": [],\n "access_cel": "\'centrifugo_admins\' in claims.groups"\n }\n}\n'})}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"enabled"})," - boolean option which enables OIDC integration. When it's on, it's only possible to log in to Centrifugo over OIDC. By default, ",(0,o.jsx)(n.code,{children:"false"}),". Enabling OIDC also enables validation of the required options below."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"display_name"})," \u2013 required string, name of IDP to be displayed on login button."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"issuer"})," - required string, the URL identifier of Identity Provider which will issue tokens. It's used for initializing OIDC provider and used as a base for the OIDC endpoint discovery."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"client_id"})," - required string, identifier for registered client in IDP for OIDC integration with Centrifugo."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"audience"})," - optional string, if not set Centrifugo expects access token audience (",(0,o.jsx)(n.code,{children:"aud"}),") to match configured ",(0,o.jsx)(n.code,{children:"client_id"})," value (as required by the OIDC spec)."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"redirect_uri"})," - required string, redirect URI to use."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"extra_scopes"})," - optional array of extra string scopes to request from IDP. Centrifugo always includes ",(0,o.jsx)(n.code,{children:"openid"})," scope as it's required by OpenID Connect protocol."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"access_cel"})," \u2013 required string, this is a CEL expression which describes rule for checking access to Centrifugo admin resources. For now we don't provide RBAC \u2013 when this expression returns true the user gets full access to Centrifugo admin resources. If false \u2013 no access at all. For more information about what is CEL check out ",(0,o.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," chapter where CEL expressions are used for channel permission checks."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Let's look closer at ",(0,o.jsx)(n.code,{children:"access_cel"}),". In the example above we check this based on a user group membership:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "admin_oidc": {\n ...\n "access_cel": "\'centrifugo_admins\' in claims.groups"\n }\n}\n'})}),"\n",(0,o.jsxs)(n.p,{children:["The expression may differ depending on IDP used \u2013 you can modify it to fit your case. Inside CEL you have access token ",(0,o.jsx)(n.code,{children:"claims"})," object with all claims of access token (which is JWT), so custom logic is possible. If you want to allow all authenticated users to access Centrifugo admin resources \u2013 then you can do the following:"]}),"\n",(0,o.jsx)(n.admonition,{type:"caution",children:(0,o.jsx)(n.p,{children:"This is usually not recommended, since every new user in your IDP will get access to Centrifugo admin UI. Deciding based on groups or some other token attribute is more secure and flexible."})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "access_cel": "true"\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},96589:(e,n,i)=>{i.d(n,{Z:()=>o});const o=i.p+"assets/images/admin_idp_auth-92fc159f4d015d269d1666453dbee5e3.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>t});var o=i(67294);const s={},r=o.createContext(s);function t(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7401],{82923:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>t,default:()=>u,frontMatter:()=>r,metadata:()=>c,toc:()=>d});var o=i(85893),s=i(11151);const r={id:"admin_idp_auth",sidebar_label:"SSO for admin UI (OIDC)",title:"SSO for admin UI using OpenID connect (OIDC)"},t=void 0,c={id:"pro/admin_idp_auth",title:"SSO for admin UI using OpenID connect (OIDC)",description:"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",source:"@site/docs/pro/admin_idp_auth.md",sourceDirName:"pro",slug:"/pro/admin_idp_auth",permalink:"/docs/pro/admin_idp_auth",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/admin_idp_auth.md",tags:[],version:"current",frontMatter:{id:"admin_idp_auth",sidebar_label:"SSO for admin UI (OIDC)",title:"SSO for admin UI using OpenID connect (OIDC)"},sidebar:"Pro",previous:{title:"Push notification API",permalink:"/docs/pro/push_notifications"},next:{title:"User status API",permalink:"/docs/pro/user_status"}},a={},d=[{value:"How it works",id:"how-it-works",level:2},{value:"Configuration",id:"configuration",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["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 ",(0,o.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Identity_provider",children:"Identity Providers"})," (IDP), such as Okta, KeyCloak, Google Workspace, Azure and others. Most of the modern providers which support ",(0,o.jsx)(n.a,{href:"https://openid.net/specs/openid-connect-core-1_0.html",children:"OpenID connect"})," (OIDC) protocol with ",(0,o.jsx)(n.a,{href:"https://oauth.net/2/pkce/",children:"Proof Key for Code Exchange"}),"\n(PKCE) and ",(0,o.jsx)(n.a,{href:"https://openid.net/specs/openid-connect-discovery-1_0.html",children:"OpenID Connect Discovery"})," are supported. This provides a way to integrate Centrifugo PRO into your existing ",(0,o.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Single_sign-on",children:"Single Sign-On"})," (SSO) infrastructure."]}),"\n",(0,o.jsx)(n.h2,{id:"how-it-works",children:"How it works"}),"\n",(0,o.jsx)(n.p,{children:"As soon as OIDC integration configured, instead of password field Centrifugo PRO admin web UI shows a button to log in using a configured Identity Provider. As soon as user successfully logs in over the IDP, user is redirected back to Centrifugo admin UI. Centrifugo checks user's access token and permissions to access admin functionality upon every request to admin resources."}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.img,{src:i(59761).Z+"",width:"1972",height:"1012"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "admin_oidc": {\n "enabled": true,\n "display_name": "Keycloak",\n "issuer": "http://localhost:8080/realms/master",\n "client_id": "myclient",\n "audience": "myclient",\n "redirect_uri": "http://localhost:8000",\n "extra_scopes": [],\n "access_cel": "\'centrifugo_admins\' in claims.groups"\n }\n}\n'})}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"enabled"})," - boolean option which enables OIDC integration. When it's on, it's only possible to log in to Centrifugo over OIDC. By default, ",(0,o.jsx)(n.code,{children:"false"}),". Enabling OIDC also enables validation of the required options below."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"display_name"})," \u2013 required string, name of IDP to be displayed on login button."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"issuer"})," - required string, the URL identifier of Identity Provider which will issue tokens. It's used for initializing OIDC provider and used as a base for the OIDC endpoint discovery."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"client_id"})," - required string, identifier for registered client in IDP for OIDC integration with Centrifugo."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"audience"})," - optional string, if not set Centrifugo expects access token audience (",(0,o.jsx)(n.code,{children:"aud"}),") to match configured ",(0,o.jsx)(n.code,{children:"client_id"})," value (as required by the OIDC spec)."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"redirect_uri"})," - required string, redirect URI to use."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"extra_scopes"})," - optional array of extra string scopes to request from IDP. Centrifugo always includes ",(0,o.jsx)(n.code,{children:"openid"})," scope as it's required by OpenID Connect protocol."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"access_cel"})," \u2013 required string, this is a CEL expression which describes rule for checking access to Centrifugo admin resources. For now we don't provide RBAC \u2013 when this expression returns true the user gets full access to Centrifugo admin resources. If false \u2013 no access at all. For more information about what is CEL check out ",(0,o.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," chapter where CEL expressions are used for channel permission checks."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Let's look closer at ",(0,o.jsx)(n.code,{children:"access_cel"}),". In the example above we check this based on a user group membership:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "admin_oidc": {\n ...\n "access_cel": "\'centrifugo_admins\' in claims.groups"\n }\n}\n'})}),"\n",(0,o.jsxs)(n.p,{children:["The expression may differ depending on IDP used \u2013 you can modify it to fit your case. Inside CEL you have access token ",(0,o.jsx)(n.code,{children:"claims"})," object with all claims of access token (which is JWT), so custom logic is possible. If you want to allow all authenticated users to access Centrifugo admin resources \u2013 then you can do the following:"]}),"\n",(0,o.jsx)(n.admonition,{type:"caution",children:(0,o.jsx)(n.p,{children:"This is usually not recommended, since every new user in your IDP will get access to Centrifugo admin UI. Deciding based on groups or some other token attribute is more secure and flexible."})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "access_cel": "true"\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},59761:(e,n,i)=>{i.d(n,{Z:()=>o});const o=i.p+"assets/images/admin_idp_auth-92fc159f4d015d269d1666453dbee5e3.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>t});var o=i(67294);const s={},r=o.createContext(s);function t(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0d57d15e.8271dc55.js b/assets/js/0d57d15e.48ceaaaa.js similarity index 98% rename from assets/js/0d57d15e.8271dc55.js rename to assets/js/0d57d15e.48ceaaaa.js index e7e0edda3..f4f6c0037 100644 --- a/assets/js/0d57d15e.8271dc55.js +++ b/assets/js/0d57d15e.48ceaaaa.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8246],{39065:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=t(85893),i=t(11151);const r={id:"integration",title:"Integration guide"},s=void 0,a={id:"getting-started/integration",title:"Integration guide",description:"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.",source:"@site/versioned_docs/version-3/getting-started/integration.md",sourceDirName:"getting-started",slug:"/getting-started/integration",permalink:"/docs/3/getting-started/integration",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/integration.md",tags:[],version:"3",frontMatter:{id:"integration",title:"Integration guide"},sidebar:"Introduction",previous:{title:"Client API showcase",permalink:"/docs/3/getting-started/client_api"},next:{title:"Design overview",permalink:"/docs/3/getting-started/design"}},c={},l=[{value:"0. Install",id:"0-install",level:2},{value:"1. Configure Centrifugo",id:"1-configure-centrifugo",level:2},{value:"2. Configure your backend",id:"2-configure-your-backend",level:2},{value:"3. Connect to Centrifugo",id:"3-connect-to-centrifugo",level:2},{value:"4. Subscribe to channels",id:"4-subscribe-to-channels",level:2},{value:"5. Publish to channel",id:"5-publish-to-channel",level:2},{value:"6. Deploy to production",id:"6-deploy-to-production",level:2},{value:"7. Monitor Centrifugo",id:"7-monitor-centrifugo",level:2},{value:"8. Scale Centrifugo",id:"8-scale-centrifugo",level:2},{value:"9. Read FAQ",id:"9-read-faq",level:2}];function d(e){const n={a:"a",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.p,{children:"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."}),"\n",(0,o.jsx)(n.p,{children:"As Centrifugo is language-agnostic and can be used together with any language/framework we won't be specific here about any backend or frontend technology your application can be built with. Only abstract steps which you can extrapolate to your application stack."}),"\n",(0,o.jsx)(n.p,{children:"Let's look again at a simplified scheme:"}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.img,{alt:"Centrifugo scheme",src:t(88687).Z+"",width:"1186",height:"626"})}),"\n",(0,o.jsx)(n.p,{children:"There are three parts involved in the idiomatic Centrifugo usage scenario: your clients (frontend application), your application backend, and Centrifugo. It's possible to use Centrifugo without any application backend involved but here we won't consider this use case."}),"\n",(0,o.jsx)(n.p,{children:"Here let's suppose you already have 2 of 3 elements: clients and backend. Now you want to add Centrifugo to receive real-time events on the client-side."}),"\n",(0,o.jsx)(n.h2,{id:"0-install",children:"0. Install"}),"\n",(0,o.jsxs)(n.p,{children:["First, you need to do is download/install Centrifugo server. See ",(0,o.jsx)(n.a,{href:"/docs/3/getting-started/installation",children:"install"})," chapter for details."]}),"\n",(0,o.jsx)(n.h2,{id:"1-configure-centrifugo",children:"1. Configure Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Create basic configuration file with ",(0,o.jsx)(n.code,{children:"token_hmac_secret_key"})," (or ",(0,o.jsx)(n.code,{children:"token_rsa_public_key"}),") and ",(0,o.jsx)(n.code,{children:"api_key"})," set and then run Centrifugo. See ",(0,o.jsx)(n.a,{href:"/docs/3/server/configuration",children:"this chapter"})," for details about ",(0,o.jsx)(n.code,{children:"token_hmac_secret_key"}),"/",(0,o.jsx)(n.code,{children:"token_rsa_public_key"})," and ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api",children:"chapter about server API"})," for API description. The simplest way to do this automatically is by using ",(0,o.jsx)(n.code,{children:"genconfig"})," command:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"./centrifugo genconfig\n"})}),"\n",(0,o.jsxs)(n.p,{children:["\u2013 which will generate ",(0,o.jsx)(n.code,{children:"config.json"})," file for you with all required fields."]}),"\n",(0,o.jsxs)(n.p,{children:["Properly configure ",(0,o.jsx)(n.a,{href:"/docs/3/server/configuration#allowed_origins",children:"allowed_origins"})," option."]}),"\n",(0,o.jsx)(n.h2,{id:"2-configure-your-backend",children:"2. Configure your backend"}),"\n",(0,o.jsxs)(n.p,{children:["In the configuration file ",(0,o.jsx)(n.strong,{children:"of your application backend"})," register several variables: Centrifugo secret and Centrifugo API key you set on a previous step and Centrifugo API address. By default, the API address is ",(0,o.jsx)(n.code,{children:"http://localhost:8000/api"}),". You ",(0,o.jsx)(n.strong,{children:"must never reveal token secret and API key to your users"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"3-connect-to-centrifugo",children:"3. Connect to Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Now your users can start connecting to Centrifugo. You should get a client library (see ",(0,o.jsx)(n.a,{href:"/docs/3/transports/client_sdk",children:"list of available client SDK"}),") for your application frontend. Every library has a method to connect to Centrifugo. See information about Centrifugo connection endpoints ",(0,o.jsx)(n.a,{href:"https://centrifugal.github.io/centrifugo/server/configuration/#advanced-endpoint-configuration",children:"here"}),". Every client should provide a connection token (JWT) on connect. You must generate this token on your backend side using Centrifugo secret key you set to backend configuration (note that in the case of RSA tokens you are generating JWT with a private key). See how to generate this JWT ",(0,o.jsx)(n.a,{href:"/docs/3/server/authentication",children:"in special chapter"}),". You pass this token from the backend to your frontend app (pass it in template context or use separate request from client-side to get user-specific JWT from backend side). And use this token when connecting to Centrifugo (for example browser client has a special method ",(0,o.jsx)(n.code,{children:"setToken"}),")."]}),"\n",(0,o.jsxs)(n.p,{children:["There is also a way to authenticate connections without using JWT - see ",(0,o.jsx)(n.a,{href:"/docs/3/server/proxy",children:"chapter about proxying to backend"}),"."]}),"\n",(0,o.jsx)(n.p,{children:"You are connecting to Centrifugo using one of the available transports. At this moment you can choose from:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["WebSocket, with JSON or binary protobuf protocol. See more info in a chapter about ",(0,o.jsx)(n.a,{href:"/docs/3/transports/websocket",children:"WebSocket transport"})]}),"\n",(0,o.jsxs)(n.li,{children:["SockJS (only supports JSON protocol). See more info about ",(0,o.jsx)(n.a,{href:"/docs/3/transports/sockjs",children:"SockJS transport"})]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"4-subscribe-to-channels",children:"4. Subscribe to channels"}),"\n",(0,o.jsxs)(n.p,{children:["After connecting to Centrifugo subscribe clients to channels they are interested in. See more about channels in ",(0,o.jsx)(n.a,{href:"/docs/3/server/channels",children:"special chapter"}),". All client libraries provide a way to handle messages coming to a client from a channel after subscribing to it."]}),"\n",(0,o.jsxs)(n.p,{children:["There is also a way to subscribe connection to a list of channels on the server side at the moment of connection establishment. See chapter about ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"5-publish-to-channel",children:"5. Publish to channel"}),"\n",(0,o.jsxs)(n.p,{children:["So everything should work now \u2013 as soon as a user opens some page of your application it must successfully connect to Centrifugo and subscribe to a channel (or channels). Now let's imagine you want to send a real-time message to users subscribed on a specific channel. This message can be a reaction to some event that happened in your app: someone posted a new comment, the administrator just created a new post, the user pressed the like button, etc. Anyway, this is an event your backend just got, and you want to immediately share it with interested users. You can do this using Centrifugo ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api",children:"HTTP API"}),". To simplify your life ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#http-api-libraries",children:"we have several API libraries"})," for different languages. You can publish messages into a channel using one of those libraries or you can simply ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#http-api",children:"follow API description"})," to construct API requests yourself - this is very simple. Also Centrifugo supports ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#grpc-api",children:"GRPC API"}),". As soon as you published a message to the channel it must be delivered to your client."]}),"\n",(0,o.jsx)(n.h2,{id:"6-deploy-to-production",children:"6. Deploy to production"}),"\n",(0,o.jsxs)(n.p,{children:["To put this all into production you need to deploy Centrifugo on your production server. To help you with this we have many things like Docker image, ",(0,o.jsx)(n.code,{children:"rpm"})," and ",(0,o.jsx)(n.code,{children:"deb"})," packages, Nginx configuration. See ",(0,o.jsx)(n.a,{href:"/docs/3/server/infra_tuning",children:"Infrastructure tuning"})," chapter for some actions you have to do to prepare your server infrastructure for handling many persistent connections."]}),"\n",(0,o.jsx)(n.h2,{id:"7-monitor-centrifugo",children:"7. Monitor Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Don't forget to ",(0,o.jsx)(n.a,{href:"/docs/3/server/monitoring",children:"monitor"})," your production Centrifugo setup."]}),"\n",(0,o.jsx)(n.h2,{id:"8-scale-centrifugo",children:"8. Scale Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["As soon as you are close to machine resource limits you may want to scale Centrifugo \u2013 you can run many Centrifugo instances and load-balance clients between them using ",(0,o.jsx)(n.a,{href:"/docs/3/server/engines",children:"Redis engine"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"9-read-faq",children:"9. Read FAQ"}),"\n",(0,o.jsxs)(n.p,{children:["That's all for basics. The documentation actually covers lots of other concepts Centrifugo server has: scalability, private channels, admin web interface, SockJS fallback, Protobuf support, and more. And don't forget to read our ",(0,o.jsx)(n.a,{href:"/docs/3/faq/",children:"FAQ"}),"."]})]})}function u(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},88687:(e,n,t)=>{t.d(n,{Z:()=>o});const o=t.p+"assets/images/scheme_sketch-74c962b2089dc49399e093b1e9812403.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>s});var o=t(67294);const i={},r=o.createContext(i);function s(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8246],{39065:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=t(85893),i=t(11151);const r={id:"integration",title:"Integration guide"},s=void 0,a={id:"getting-started/integration",title:"Integration guide",description:"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.",source:"@site/versioned_docs/version-3/getting-started/integration.md",sourceDirName:"getting-started",slug:"/getting-started/integration",permalink:"/docs/3/getting-started/integration",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/integration.md",tags:[],version:"3",frontMatter:{id:"integration",title:"Integration guide"},sidebar:"Introduction",previous:{title:"Client API showcase",permalink:"/docs/3/getting-started/client_api"},next:{title:"Design overview",permalink:"/docs/3/getting-started/design"}},c={},l=[{value:"0. Install",id:"0-install",level:2},{value:"1. Configure Centrifugo",id:"1-configure-centrifugo",level:2},{value:"2. Configure your backend",id:"2-configure-your-backend",level:2},{value:"3. Connect to Centrifugo",id:"3-connect-to-centrifugo",level:2},{value:"4. Subscribe to channels",id:"4-subscribe-to-channels",level:2},{value:"5. Publish to channel",id:"5-publish-to-channel",level:2},{value:"6. Deploy to production",id:"6-deploy-to-production",level:2},{value:"7. Monitor Centrifugo",id:"7-monitor-centrifugo",level:2},{value:"8. Scale Centrifugo",id:"8-scale-centrifugo",level:2},{value:"9. Read FAQ",id:"9-read-faq",level:2}];function d(e){const n={a:"a",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.p,{children:"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."}),"\n",(0,o.jsx)(n.p,{children:"As Centrifugo is language-agnostic and can be used together with any language/framework we won't be specific here about any backend or frontend technology your application can be built with. Only abstract steps which you can extrapolate to your application stack."}),"\n",(0,o.jsx)(n.p,{children:"Let's look again at a simplified scheme:"}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.img,{alt:"Centrifugo scheme",src:t(15501).Z+"",width:"1186",height:"626"})}),"\n",(0,o.jsx)(n.p,{children:"There are three parts involved in the idiomatic Centrifugo usage scenario: your clients (frontend application), your application backend, and Centrifugo. It's possible to use Centrifugo without any application backend involved but here we won't consider this use case."}),"\n",(0,o.jsx)(n.p,{children:"Here let's suppose you already have 2 of 3 elements: clients and backend. Now you want to add Centrifugo to receive real-time events on the client-side."}),"\n",(0,o.jsx)(n.h2,{id:"0-install",children:"0. Install"}),"\n",(0,o.jsxs)(n.p,{children:["First, you need to do is download/install Centrifugo server. See ",(0,o.jsx)(n.a,{href:"/docs/3/getting-started/installation",children:"install"})," chapter for details."]}),"\n",(0,o.jsx)(n.h2,{id:"1-configure-centrifugo",children:"1. Configure Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Create basic configuration file with ",(0,o.jsx)(n.code,{children:"token_hmac_secret_key"})," (or ",(0,o.jsx)(n.code,{children:"token_rsa_public_key"}),") and ",(0,o.jsx)(n.code,{children:"api_key"})," set and then run Centrifugo. See ",(0,o.jsx)(n.a,{href:"/docs/3/server/configuration",children:"this chapter"})," for details about ",(0,o.jsx)(n.code,{children:"token_hmac_secret_key"}),"/",(0,o.jsx)(n.code,{children:"token_rsa_public_key"})," and ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api",children:"chapter about server API"})," for API description. The simplest way to do this automatically is by using ",(0,o.jsx)(n.code,{children:"genconfig"})," command:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"./centrifugo genconfig\n"})}),"\n",(0,o.jsxs)(n.p,{children:["\u2013 which will generate ",(0,o.jsx)(n.code,{children:"config.json"})," file for you with all required fields."]}),"\n",(0,o.jsxs)(n.p,{children:["Properly configure ",(0,o.jsx)(n.a,{href:"/docs/3/server/configuration#allowed_origins",children:"allowed_origins"})," option."]}),"\n",(0,o.jsx)(n.h2,{id:"2-configure-your-backend",children:"2. Configure your backend"}),"\n",(0,o.jsxs)(n.p,{children:["In the configuration file ",(0,o.jsx)(n.strong,{children:"of your application backend"})," register several variables: Centrifugo secret and Centrifugo API key you set on a previous step and Centrifugo API address. By default, the API address is ",(0,o.jsx)(n.code,{children:"http://localhost:8000/api"}),". You ",(0,o.jsx)(n.strong,{children:"must never reveal token secret and API key to your users"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"3-connect-to-centrifugo",children:"3. Connect to Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Now your users can start connecting to Centrifugo. You should get a client library (see ",(0,o.jsx)(n.a,{href:"/docs/3/transports/client_sdk",children:"list of available client SDK"}),") for your application frontend. Every library has a method to connect to Centrifugo. See information about Centrifugo connection endpoints ",(0,o.jsx)(n.a,{href:"https://centrifugal.github.io/centrifugo/server/configuration/#advanced-endpoint-configuration",children:"here"}),". Every client should provide a connection token (JWT) on connect. You must generate this token on your backend side using Centrifugo secret key you set to backend configuration (note that in the case of RSA tokens you are generating JWT with a private key). See how to generate this JWT ",(0,o.jsx)(n.a,{href:"/docs/3/server/authentication",children:"in special chapter"}),". You pass this token from the backend to your frontend app (pass it in template context or use separate request from client-side to get user-specific JWT from backend side). And use this token when connecting to Centrifugo (for example browser client has a special method ",(0,o.jsx)(n.code,{children:"setToken"}),")."]}),"\n",(0,o.jsxs)(n.p,{children:["There is also a way to authenticate connections without using JWT - see ",(0,o.jsx)(n.a,{href:"/docs/3/server/proxy",children:"chapter about proxying to backend"}),"."]}),"\n",(0,o.jsx)(n.p,{children:"You are connecting to Centrifugo using one of the available transports. At this moment you can choose from:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:["WebSocket, with JSON or binary protobuf protocol. See more info in a chapter about ",(0,o.jsx)(n.a,{href:"/docs/3/transports/websocket",children:"WebSocket transport"})]}),"\n",(0,o.jsxs)(n.li,{children:["SockJS (only supports JSON protocol). See more info about ",(0,o.jsx)(n.a,{href:"/docs/3/transports/sockjs",children:"SockJS transport"})]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"4-subscribe-to-channels",children:"4. Subscribe to channels"}),"\n",(0,o.jsxs)(n.p,{children:["After connecting to Centrifugo subscribe clients to channels they are interested in. See more about channels in ",(0,o.jsx)(n.a,{href:"/docs/3/server/channels",children:"special chapter"}),". All client libraries provide a way to handle messages coming to a client from a channel after subscribing to it."]}),"\n",(0,o.jsxs)(n.p,{children:["There is also a way to subscribe connection to a list of channels on the server side at the moment of connection establishment. See chapter about ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"5-publish-to-channel",children:"5. Publish to channel"}),"\n",(0,o.jsxs)(n.p,{children:["So everything should work now \u2013 as soon as a user opens some page of your application it must successfully connect to Centrifugo and subscribe to a channel (or channels). Now let's imagine you want to send a real-time message to users subscribed on a specific channel. This message can be a reaction to some event that happened in your app: someone posted a new comment, the administrator just created a new post, the user pressed the like button, etc. Anyway, this is an event your backend just got, and you want to immediately share it with interested users. You can do this using Centrifugo ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api",children:"HTTP API"}),". To simplify your life ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#http-api-libraries",children:"we have several API libraries"})," for different languages. You can publish messages into a channel using one of those libraries or you can simply ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#http-api",children:"follow API description"})," to construct API requests yourself - this is very simple. Also Centrifugo supports ",(0,o.jsx)(n.a,{href:"/docs/3/server/server_api#grpc-api",children:"GRPC API"}),". As soon as you published a message to the channel it must be delivered to your client."]}),"\n",(0,o.jsx)(n.h2,{id:"6-deploy-to-production",children:"6. Deploy to production"}),"\n",(0,o.jsxs)(n.p,{children:["To put this all into production you need to deploy Centrifugo on your production server. To help you with this we have many things like Docker image, ",(0,o.jsx)(n.code,{children:"rpm"})," and ",(0,o.jsx)(n.code,{children:"deb"})," packages, Nginx configuration. See ",(0,o.jsx)(n.a,{href:"/docs/3/server/infra_tuning",children:"Infrastructure tuning"})," chapter for some actions you have to do to prepare your server infrastructure for handling many persistent connections."]}),"\n",(0,o.jsx)(n.h2,{id:"7-monitor-centrifugo",children:"7. Monitor Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["Don't forget to ",(0,o.jsx)(n.a,{href:"/docs/3/server/monitoring",children:"monitor"})," your production Centrifugo setup."]}),"\n",(0,o.jsx)(n.h2,{id:"8-scale-centrifugo",children:"8. Scale Centrifugo"}),"\n",(0,o.jsxs)(n.p,{children:["As soon as you are close to machine resource limits you may want to scale Centrifugo \u2013 you can run many Centrifugo instances and load-balance clients between them using ",(0,o.jsx)(n.a,{href:"/docs/3/server/engines",children:"Redis engine"}),"."]}),"\n",(0,o.jsx)(n.h2,{id:"9-read-faq",children:"9. Read FAQ"}),"\n",(0,o.jsxs)(n.p,{children:["That's all for basics. The documentation actually covers lots of other concepts Centrifugo server has: scalability, private channels, admin web interface, SockJS fallback, Protobuf support, and more. And don't forget to read our ",(0,o.jsx)(n.a,{href:"/docs/3/faq/",children:"FAQ"}),"."]})]})}function u(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},15501:(e,n,t)=>{t.d(n,{Z:()=>o});const o=t.p+"assets/images/scheme_sketch-74c962b2089dc49399e093b1e9812403.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>s});var o=t(67294);const i={},r=o.createContext(i);function s(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0dc36dc4.5a84cb5d.js b/assets/js/0dc36dc4.972ce2f0.js similarity index 96% rename from assets/js/0dc36dc4.5a84cb5d.js rename to assets/js/0dc36dc4.972ce2f0.js index e8a0e5bc1..d32cdcdbb 100644 --- a/assets/js/0dc36dc4.5a84cb5d.js +++ b/assets/js/0dc36dc4.972ce2f0.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6990],{15594:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>a,metadata:()=>s,toc:()=>l});var i=n(85893),o=n(11151);const a={id:"introduction",title:"Centrifugo introduction"},r=void 0,s={id:"getting-started/introduction",title:"Centrifugo introduction",description:"Centrifugo is an open-source scalable real-time messaging server. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, SSE/EventSource, WebTransport, GRPC, SockJS). Centrifugo has the concept of a channel \u2013 so it's a user-facing PUB/SUB server.",source:"@site/versioned_docs/version-4/getting-started/introduction.md",sourceDirName:"getting-started",slug:"/getting-started/introduction",permalink:"/docs/4/getting-started/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/getting-started/introduction.md",tags:[],version:"4",frontMatter:{id:"introduction",title:"Centrifugo introduction"},sidebar:"Introduction",next:{title:"Join community",permalink:"/docs/4/getting-started/community"}},c={},l=[{value:"Background",id:"background",level:2}];function d(e){const t={admonition:"admonition",h2:"h2",img:"img",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:"/img/logo_animated_no_accel.svg",width:"100px",height:"100px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is an open-source scalable real-time messaging server. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, SSE/EventSource, WebTransport, GRPC, SockJS). Centrifugo has the concept of a channel \u2013 so it's a user-facing PUB/SUB server."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is language-agnostic and can be used to build chat apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, etc. in combination with any backend. It is well suited for modern architectures and allows decoupling the business logic from the real-time transport layer."}),"\n",(0,i.jsx)(t.p,{children:"Several official client SDKs for browser and mobile development wrap the bidirectional protocol. In addition, Centrifugo supports a unidirectional approach for simple use cases with no SDK dependency."}),"\n",(0,i.jsx)(t.admonition,{title:"Real-time?",type:"info",children:(0,i.jsx)(t.p,{children:"By real-time, we indicate a soft real-time. Due to network latencies, garbage collection cycles, and so on, the delay of a delivered message can be up to several hundred milliseconds or higher."})}),"\n",(0,i.jsx)(t.h2,{id:"background",children:"Background"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{src:n(30740).Z+"",width:"2000",height:"1068"})}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo was born a decade ago to help applications with a server-side written in a language or a framework without built-in concurrency support. In this case, dealing with persistent connections is a real headache that usually can only be resolved by introducing a shift in the technology stack and spending time to create a production-ready solution."}),"\n",(0,i.jsx)(t.p,{children:"For example, frameworks like Django, Flask, Yii, Laravel, Ruby on Rails, and others have poor or not really performant support of working with many persistent connections for the real-time messaging tasks."}),"\n",(0,i.jsx)(t.p,{children:"In this case, Centrifugo is a straightforward and non-obtrusive way to introduce real-time updates and handle lots of persistent connections without radical changes in the application backend architecture. Developers could proceed writing the application backend with a favorite language or favorite framework, keep existing architecture \u2013 and just let Centrifugo deal with persistent connections and be a real-time messaging transport layer."}),"\n",(0,i.jsx)(t.p,{children:"These days Centrifugo provides some advanced and unique features that can simplify a developer's life and save months of development. Even if the application backend is built with the asynchronous concurrent language. One example is that Centrifugo has built-in support for scalability to many machines to handle more connections and still making sure channel subscribers on different Centrifugo nodes receive all the publications."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo fits well modern architectures and may be a universal real-time component regardless of the application technology stack. There are more things to mention, the documentation uncovers them step by step."})]})}function u(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},30740:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/bg_cat-4454fbaae0446c3b1964e06821dd378b.jpg"},11151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var i=n(67294);const o={},a=i.createContext(o);function r(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6990],{15594:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>a,metadata:()=>s,toc:()=>l});var i=n(85893),o=n(11151);const a={id:"introduction",title:"Centrifugo introduction"},r=void 0,s={id:"getting-started/introduction",title:"Centrifugo introduction",description:"Centrifugo is an open-source scalable real-time messaging server. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, SSE/EventSource, WebTransport, GRPC, SockJS). Centrifugo has the concept of a channel \u2013 so it's a user-facing PUB/SUB server.",source:"@site/versioned_docs/version-4/getting-started/introduction.md",sourceDirName:"getting-started",slug:"/getting-started/introduction",permalink:"/docs/4/getting-started/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/getting-started/introduction.md",tags:[],version:"4",frontMatter:{id:"introduction",title:"Centrifugo introduction"},sidebar:"Introduction",next:{title:"Join community",permalink:"/docs/4/getting-started/community"}},c={},l=[{value:"Background",id:"background",level:2}];function d(e){const t={admonition:"admonition",h2:"h2",img:"img",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:"/img/logo_animated_no_accel.svg",width:"100px",height:"100px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is an open-source scalable real-time messaging server. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, SSE/EventSource, WebTransport, GRPC, SockJS). Centrifugo has the concept of a channel \u2013 so it's a user-facing PUB/SUB server."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo is language-agnostic and can be used to build chat apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, etc. in combination with any backend. It is well suited for modern architectures and allows decoupling the business logic from the real-time transport layer."}),"\n",(0,i.jsx)(t.p,{children:"Several official client SDKs for browser and mobile development wrap the bidirectional protocol. In addition, Centrifugo supports a unidirectional approach for simple use cases with no SDK dependency."}),"\n",(0,i.jsx)(t.admonition,{title:"Real-time?",type:"info",children:(0,i.jsx)(t.p,{children:"By real-time, we indicate a soft real-time. Due to network latencies, garbage collection cycles, and so on, the delay of a delivered message can be up to several hundred milliseconds or higher."})}),"\n",(0,i.jsx)(t.h2,{id:"background",children:"Background"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{src:n(72297).Z+"",width:"2000",height:"1068"})}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo was born a decade ago to help applications with a server-side written in a language or a framework without built-in concurrency support. In this case, dealing with persistent connections is a real headache that usually can only be resolved by introducing a shift in the technology stack and spending time to create a production-ready solution."}),"\n",(0,i.jsx)(t.p,{children:"For example, frameworks like Django, Flask, Yii, Laravel, Ruby on Rails, and others have poor or not really performant support of working with many persistent connections for the real-time messaging tasks."}),"\n",(0,i.jsx)(t.p,{children:"In this case, Centrifugo is a straightforward and non-obtrusive way to introduce real-time updates and handle lots of persistent connections without radical changes in the application backend architecture. Developers could proceed writing the application backend with a favorite language or favorite framework, keep existing architecture \u2013 and just let Centrifugo deal with persistent connections and be a real-time messaging transport layer."}),"\n",(0,i.jsx)(t.p,{children:"These days Centrifugo provides some advanced and unique features that can simplify a developer's life and save months of development. Even if the application backend is built with the asynchronous concurrent language. One example is that Centrifugo has built-in support for scalability to many machines to handle more connections and still making sure channel subscribers on different Centrifugo nodes receive all the publications."}),"\n",(0,i.jsx)(t.p,{children:"Centrifugo fits well modern architectures and may be a universal real-time component regardless of the application technology stack. There are more things to mention, the documentation uncovers them step by step."})]})}function u(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},72297:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/bg_cat-4454fbaae0446c3b1964e06821dd378b.jpg"},11151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var i=n(67294);const o={},a=i.createContext(o);function r(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0eae5577.40a9360f.js b/assets/js/0eae5577.40a9360f.js new file mode 100644 index 000000000..c4a68c349 --- /dev/null +++ b/assets/js/0eae5577.40a9360f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5048],{41183:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>u,contentTitle:()=>c,default:()=>p,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var s=i(85893),t=i(11151),o=i(74866),a=i(85162);const r={title:"101 ways to subscribe user on a personal channel in Centrifugo",tags:["centrifugo","tutorial"],description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/101-way_thumb.jpg",hide_table_of_contents:!1},c=void 0,l={permalink:"/blog/2022/07/29/101-way-to-subscribe",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2022-07-29-101-way-to-subscribe.md",source:"@site/blog/2022-07-29-101-way-to-subscribe.md",title:"101 ways to subscribe user on a personal channel in Centrifugo",description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",date:"2022-07-29T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"tutorial",permalink:"/blog/tags/tutorial"}],readingTime:10.64,hasTruncateMarker:!0,authors:[{name:"Alexander Emelin",title:"Author of Centrifugo",imageURL:"https://github.com/FZambia.png"}],frontMatter:{title:"101 ways to subscribe user on a personal channel in Centrifugo",tags:["centrifugo","tutorial"],description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/101-way_thumb.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",permalink:"/blog/2022/12/20/improving-redis-engine-performance"},nextItem:{title:"Centrifugo v4 released \u2013 a little revolution",permalink:"/blog/2022/07/19/centrifugo-v4-released"}},u={authorsImageUrls:[void 0]},d=[{value:"Setup",id:"setup",level:2},{value:"#1 \u2013 user-limited channel",id:"1--user-limited-channel",level:2},{value:"#2 - channel token authorization",id:"2---channel-token-authorization",level:2},{value:"#3 - subscribe proxy",id:"3---subscribe-proxy",level:2},{value:"#4 - server-side channel in connection JWT",id:"4---server-side-channel-in-connection-jwt",level:2},{value:"#5 - server-side channel in connect proxy",id:"5---server-side-channel-in-connect-proxy",level:2},{value:"#6 - automatic personal channel subscription",id:"6---automatic-personal-channel-subscription",level:2},{value:"#7 \u2013 capabilities in connection JWT",id:"7--capabilities-in-connection-jwt",level:2},{value:"#8 \u2013 capabilities in connect proxy",id:"8--capabilities-in-connect-proxy",level:2},{value:"Teardown",id:"teardown",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifuge",src:i(51102).Z+"",width:"1600",height:"542"})}),"\n",(0,s.jsx)(n.p,{children:"Let's say you develop an application and want a real-time connection which is subscribed to one channel. Let's also assume that this channel is used for user personal notifications. So only one user in the application can subcribe to that channel to receive its notifications in real-time."}),"\n",(0,s.jsx)(n.p,{children:"In this post we will look at various ways to achieve this with Centrifugo, and consider trade-offs of the available approaches. The main goal of this tutorial is to help Centrifugo newcomers be aware of all the ways to control channel permissions by reading just one document."}),"\n",(0,s.jsx)(n.p,{children:"And... well, there are actually 8 ways I found, not 101 \ud83d\ude07"}),"\n",(0,s.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,s.jsxs)(n.p,{children:["To make the post a bit easier to consume let's setup some things. Let's assume that the user for which we provide all the examples in this post has ID ",(0,s.jsx)(n.code,{children:'"17"'}),". Of course in real-life the examples given here can be extrapolated to any user ID."]}),"\n",(0,s.jsx)(n.p,{children:"When you create a real-time connection to Centrifugo the connection is authenticated using the one of the following ways:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["using ",(0,s.jsx)(n.a,{href:"/docs/server/authentication",children:"connection JWT"})]}),"\n",(0,s.jsxs)(n.li,{children:["using connection request proxy from Centrifugo to the configured endpoint of the application backend (",(0,s.jsx)(n.a,{href:"/docs/server/proxy#connect-proxy",children:"connect proxy"}),")"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"As soon as the connection is successfully established and authenticated Centrifugo knows the ID of connected user. This is important to understand."}),"\n",(0,s.jsx)(n.p,{children:"And let's define a namespace in Centrifugo configuration which will be used for personal user channels:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n ...\n "namespaces": [\n {\n "name": "personal",\n "presence": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Defining namespaces for each new real-time feature is a good practice in Centrifugo. As an awesome improvement we also enabled ",(0,s.jsx)(n.code,{children:"presence"})," in the ",(0,s.jsx)(n.code,{children:"personal"})," namespace, so whenever users subscribe to a channel in this namespace Centrifugo will maintain online presence information for each channel. So you can find out all connections of the specific user existing at any moment. Defining ",(0,s.jsx)(n.code,{children:"presence"})," is fully optional though - turn it of if you don't need presence information and don't want to spend additional server resources on maintaining presence."]}),"\n",(0,s.jsx)(n.h2,{id:"1--user-limited-channel",children:"#1 \u2013 user-limited channel"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most performant approach."})}),"\n",(0,s.jsxs)(n.p,{children:["All you need to do is to extend namespace configuration with ",(0,s.jsx)(n.code,{children:"allow_user_limited_channels"})," option:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "personal",\n "presence": true,\n "allow_user_limited_channels": true\n }\n ]\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"On the client side you need to have sth like this (of course the ID of current user will be dynamic in real-life):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:#17');\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Here you are subscribing to a channel in ",(0,s.jsx)(n.code,{children:"personal"})," namespace and listening to publications coming from a channel. Having ",(0,s.jsx)(n.code,{children:"#"})," in channel name tells Centrifugo that this is a user-limited channel (because ",(0,s.jsx)(n.code,{children:"#"})," is a special symbol that is treated in a special way by Centrifugo as soon as ",(0,s.jsx)(n.code,{children:"allow_user_limited_channels"})," enabled)."]}),"\n",(0,s.jsxs)(n.p,{children:["In this case the user ID part of user-limited channel is ",(0,s.jsx)(n.code,{children:'"17"'}),". So Centrifugo allows user with ID ",(0,s.jsx)(n.code,{children:'"17"'})," to subscribe on ",(0,s.jsx)(n.code,{children:"personal:#17"})," channel. Other users won't be able to subscribe on it."]}),"\n",(0,s.jsxs)(n.p,{children:["To publish updates to subscription all you need to do is to publish to ",(0,s.jsx)(n.code,{children:"personal:#17"})," using server publish API (HTTP or GRPC)."]}),"\n",(0,s.jsx)(n.h2,{id:"2---channel-token-authorization",children:"#2 - channel token authorization"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most flexible approach, with reasonably good performance characteristics."})}),"\n",(0,s.jsx)(n.p,{children:"Another way we will look at is using subscription JWT for subscribing. When you create Subscription object on the client side you can pass it a subscription token, and also provide a function to retrieve subscription token (useful to automatically handle token refresh, it also handles initial token loading)."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const token = await getSubscriptionToken('personal:17');\n\nconst sub = centrifuge.newSubscription('personal:17', {\n token: token\n});\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Inside ",(0,s.jsx)(n.code,{children:"getSubscriptionToken"})," you can issue a request to the backend, for example in browser it's possible to do with fetch API."]}),"\n",(0,s.jsxs)(n.p,{children:["On the backend side you know the ID of current user due to the native session mechanism of your app, so you can decide whether current user has permission to subsribe on ",(0,s.jsx)(n.code,{children:"personal:17"})," or not. If yes \u2013 return subscription JWT according to our rules. If not - return empty string so subscription will go to unsubscribed state with ",(0,s.jsx)(n.code,{children:"unauthorized"})," reason."]}),"\n",(0,s.jsxs)(n.p,{children:["Here are examples for generating subscription HMAC SHA-256 JWTs for channel ",(0,s.jsx)(n.code,{children:"personal:17"})," and HMAC secret key ",(0,s.jsx)(n.code,{children:"secret"}),":"]}),"\n","\n",(0,s.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,s.jsx)(a.Z,{value:"python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {\n "sub": "17",\n "channel": "personal:17"\n "exp": int(time.time()) + 30*60\n}\n\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,s.jsx)(a.Z,{value:"node",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const jose = require('jose')\n\n(async function main() {\n const secret = new TextEncoder().encode('secret')\n const alg = 'HS256'\n\n const token = await new jose.SignJWT({ 'sub': '17', 'channel': 'personal:17' })\n .setProtectedHeader({ alg })\n .setExpirationTime('30m')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Since we set expiration time for subscription JWT tokens we also need to provide a ",(0,s.jsx)(n.code,{children:"getToken"})," function to a client on the frontend side:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:17', {\n getToken: async function (ctx) {\n const token = await getSubscriptionToken('personal:17');\n return token;\n }\n});\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This function will be called by SDK automatically to refresh subscription token when it's going to expire. And note that we omitted setting ",(0,s.jsx)(n.code,{children:"token"})," option here \u2013 since SDK is smart enough to call provided ",(0,s.jsx)(n.code,{children:"getToken"})," function to extract initial subscription token from the backend."]}),"\n",(0,s.jsxs)(n.p,{children:["The good thing in using subscription JWT approach is that you can provide token expiration time, so permissions to subscribe on a channel will be validated from time to time while connection is active. You can also provide additional channel context info which will be attached to presence information (using ",(0,s.jsx)(n.code,{children:"info"})," claim of subscription JWT). And you can granularly control channel permissions using ",(0,s.jsx)(n.code,{children:"allow"})," claim of token \u2013 and give client capabilities to publish, call history or presence information (this is Centrifugo PRO feature at this point). Token also allows to override some namespace options on per-subscription basis (with ",(0,s.jsx)(n.code,{children:"override"})," claim)."]}),"\n",(0,s.jsx)(n.p,{children:"Using subscription tokens is a general approach for any channels where you need to check access first, not only for personal user channels."}),"\n",(0,s.jsx)(n.h2,{id:"3---subscribe-proxy",children:"#3 - subscribe proxy"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most secure approach."})}),"\n",(0,s.jsx)(n.p,{children:"Subscription JWT gives client a way to subscribe on a channel, and avoid requesting your backend for permission on every resubscribe. Token approach is very good in massive reconnect scenario, when you have many connections and they all resubscribe at once (due to your load balancer reload, for example). But this means that if you unsubscribed client from a channel using server API, client can still resubscribe with token again - until token will expire. In some cases you may want to avoid this."}),"\n",(0,s.jsx)(n.p,{children:"Also, in some cases you want to be notified when someone subscribes to a channel."}),"\n",(0,s.jsx)(n.p,{children:"In this case you may use subscribe proxy feature. When using subscribe proxy every attempt of a client to subscribe on a channel will be translated to request (HTTP or GRPC) from Centrifugo to the application backend. Application backend can decide whether client is allowed to subscribe or not."}),"\n",(0,s.jsxs)(n.p,{children:["One advantage of using subscribe proxy is that backend can additionally provide initial channel data for the subscribing client. This is possible using ",(0,s.jsx)(n.code,{children:"data"})," field of subscribe result generated by backend subscribe handler."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "proxy_subscribe_endpoint": "http://localhost:9000/centrifugo/subscribe",\n "namespaces": [\n {\n "name": "personal",\n "presence": true,\n "proxy_subscribe": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["And on the backend side define a route ",(0,s.jsx)(n.code,{children:"/centrifugo/subscribe"}),", check permissions of user upon subscription and return result to Centrifugo according to our subscribe proxy docs. Or simply run GRPC server using our proxy definitions and react on subscription attempt sent from Centrifugo to backend over GRPC."]}),"\n",(0,s.jsx)(n.p,{children:"On the client-side code is as simple as:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:17');\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"4---server-side-channel-in-connection-jwt",children:"#4 - server-side channel in connection JWT"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"The approach where you don't need to manage client-side subscriptions."})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"/docs/server/server_subs",children:"Server-side subscriptions"})," is a way to consume publications from channels without even create Subscription objects on the client side. In general, client side Subscription objects provide a more flexible and controllable way to work with subscriptions. Clients can subscribe/unsubscribe on channels at any point. Client-side subscriptions provide more details about state transitions."]}),"\n",(0,s.jsx)(n.p,{children:"With server-side subscriptions though you are consuming publications directly from Client instance:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {\n token: 'CONNECTION-JWT'\n});\nclient.on('publication', function(ctx) {\n console.log('publication received from server-side channel', ctx.channel, ctx.data);\n});\nclient.connect();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case you don't have separate Subscription objects and need to look at ",(0,s.jsx)(n.code,{children:"ctx.channel"})," upon receiving publication or to publication content to decide how to handle it. Server-side subscriptions could be a good choice if you are using Centrifugo unidirectional transports and don't need dynamic subscribe/unsubscribe behavior."]}),"\n",(0,s.jsxs)(n.p,{children:["The first way to subscribe client on a server-side channel is to include ",(0,s.jsx)(n.code,{children:"channels"})," claim into connection JWT:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "sub": "17",\n "channels": ["personal:17"]\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"Upon successful connection user will be subscribed to a server-side channel by Centrifugo. One downside of using server-side channels is that errors in one server-side channel (like impossible to recover missed messages) may affect the entire connection and result into reconnects, while with client-side subscriptions individual subsription failures do not affect the entire connection."}),"\n",(0,s.jsx)(n.p,{children:"But having one server-side channel per-connection seems a very reasonable idea to me in many cases. And if you have stable set of subscriptions which do not require lifetime state management \u2013 this can be a nice approach without additional protocol/network overhead involved."}),"\n",(0,s.jsx)(n.h2,{id:"5---server-side-channel-in-connect-proxy",children:"#5 - server-side channel in connect proxy"}),"\n",(0,s.jsx)(n.p,{children:"Similar to the previous one for cases when you are authenticating connections over connect proxy instead of using JWT."}),"\n",(0,s.jsxs)(n.p,{children:["This is possible using ",(0,s.jsx)(n.code,{children:"channels"})," field of connect proxy handler result. The code on the client-side is the same as in Option #4 \u2013 since we only change the way how list of server-side channels is provided."]}),"\n",(0,s.jsx)(n.h2,{id:"6---automatic-personal-channel-subscription",children:"#6 - automatic personal channel subscription"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Almost no code approach."})}),"\n",(0,s.jsx)(n.p,{children:"As we pointed above Centrifugo knows an ID of the user due to authentication process. So why not combining this knowledge with automatic server-side personal channel subscription? Centrifugo provides exactly this with user personal channel feature."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "user_subscribe_to_personal": true,\n "user_personal_channel_namespace": "personal",\n "namespaces": [\n {\n "name": "personal",\n "presence": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This feature only subscribes non-anonymous users to personal channels (those with non-empty user ID). The configuration above will subscribe our user ",(0,s.jsx)(n.code,{children:'"17"'})," to channel ",(0,s.jsx)(n.code,{children:"personal:#17"})," automatically after successful authentication."]}),"\n",(0,s.jsx)(n.h2,{id:"7--capabilities-in-connection-jwt",children:"#7 \u2013 capabilities in connection JWT"}),"\n",(0,s.jsx)(n.p,{children:"Allows using client-side subscriptions, but skip receiving subscription token. This is only available in Centrifugo PRO at this point."}),"\n",(0,s.jsxs)(n.p,{children:["So when generating JWT you can provide additional ",(0,s.jsx)(n.code,{children:"caps"})," claim which contains channel resource capabilities:"]}),"\n",(0,s.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,s.jsx)(a.Z,{value:"python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {\n "sub": "17",\n "exp": int(time.time()) + 30*60,\n "caps": [\n {\n "channels": ["personal:17"],\n "allow": ["sub"]\n }\n ]\n}\n\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,s.jsx)(a.Z,{value:"node",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const jose = require('jose');\n\n(async function main() {\n const secret = new TextEncoder().encode('secret')\n const alg = 'HS256'\n\n const token = await new jose.SignJWT({\n sub: '17',\n caps: [\n {\n \"channels\": [\"personal:17\"],\n \"allow\": [\"sub\"]\n }\n ]\n })\n .setProtectedHeader({ alg })\n .setExpirationTime('30m')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,s.jsx)(n.p,{children:"While in case of single channel the benefit of using this approach is not really obvious, it can help when you are using several channels with stric access permissions per connection, where providing capabilities can help to save some traffic and CPU resources since we avoid generating subscription token for each individual channel."}),"\n",(0,s.jsx)(n.h2,{id:"8--capabilities-in-connect-proxy",children:"#8 \u2013 capabilities in connect proxy"}),"\n",(0,s.jsx)(n.p,{children:"This is very similar to the previous approach, but capabilities are passed to Centrifugo in connect proxy result. So if you are using connect proxy for auth then you can still provide capabilities in the same form as in JWT. This is also a Centrifugo PRO feature."}),"\n",(0,s.jsx)(n.h2,{id:"teardown",children:"Teardown"}),"\n",(0,s.jsx)(n.p,{children:"Which way to choose? Well, it depends. Since your application will have more than only a personal user channel in many cases you should decide which approach suits you better in each particular case \u2013 it's hard to give the universal advice."}),"\n",(0,s.jsx)(n.p,{children:"Client-side subscriptions are more flexible in general, so I'd suggest using them whenever possible. Though you may use unidirectional transports of Centrifugo where subscribing to channels from the client side is not simple to achieve (though still possible using our server subscribe API). Server-side subscriptions make more sense there."}),"\n",(0,s.jsx)(n.p,{children:"The good news is that all our official bidirectional client SDKs support all the approaches mentioned in this post. Hope designing the channel configuration on top of Centrifugo will be a pleasant experience for you."}),"\n",(0,s.jsx)(n.admonition,{title:"Attributions",type:"note",children:(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)("a",{href:"https://www.freepik.com/vectors/internet-network",children:["Internet network vector created by rawpixel.com - ",(0,s.jsx)(n.a,{href:"http://www.freepik.com",children:"www.freepik.com"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)("a",{href:"https://www.flaticon.com/free-icons/cyber-security",title:"cyber security icons",children:"Cyber security icons created by Smashicons - Flaticon"}),"\n"]}),"\n"]})})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},85162:(e,n,i)=>{i.d(n,{Z:()=>a});i(67294);var s=i(36905);const t={tabItem:"tabItem_Ymn6"};var o=i(85893);function a(e){let{children:n,hidden:i,className:a}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,s.Z)(t.tabItem,a),hidden:i,children:n})}},74866:(e,n,i)=>{i.d(n,{Z:()=>w});var s=i(67294),t=i(36905),o=i(12466),a=i(16550),r=i(20469),c=i(91980),l=i(67392),u=i(50012);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:i}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:i,attributes:s,default:t}}=e;return{value:n,label:i,attributes:s,default:t}}))}(i);return function(e){const n=(0,l.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,i])}function p(e){let{value:n,tabValues:i}=e;return i.some((e=>e.value===n))}function b(e){let{queryString:n=!1,groupId:i}=e;const t=(0,a.k6)(),o=function(e){let{queryString:n=!1,groupId:i}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!i)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return i??null}({queryString:n,groupId:i});return[(0,c._X)(o),(0,s.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(t.location.search);n.set(o,e),t.replace({...t.location,search:n.toString()})}),[o,t])]}function m(e){const{defaultValue:n,queryString:i=!1,groupId:t}=e,o=h(e),[a,c]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:i}=e;if(0===i.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:i}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${i.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=i.find((e=>e.default))??i[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:o}))),[l,d]=b({queryString:i,groupId:t}),[m,f]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,o]=(0,u.Nk)(i);return[t,(0,s.useCallback)((e=>{i&&o.set(e)}),[i,o])]}({groupId:t}),g=(()=>{const e=l??m;return p({value:e,tabValues:o})?e:null})();(0,r.Z)((()=>{g&&c(g)}),[g]);return{selectedValue:a,selectValue:(0,s.useCallback)((e=>{if(!p({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);c(e),d(e),f(e)}),[d,f,o]),tabValues:o}}var f=i(72389);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var x=i(85893);function v(e){let{className:n,block:i,selectedValue:s,selectValue:a,tabValues:r}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),u=e=>{const n=e.currentTarget,i=c.indexOf(n),t=r[i].value;t!==s&&(l(n),a(t))},d=e=>{let n=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const i=c.indexOf(e.currentTarget)+1;n=c[i]??c[0];break}case"ArrowLeft":{const i=c.indexOf(e.currentTarget)-1;n=c[i]??c[c.length-1];break}}n?.focus()};return(0,x.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":i},n),children:r.map((e=>{let{value:n,label:i,attributes:o}=e;return(0,x.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>c.push(e),onKeyDown:d,onClick:u,...o,className:(0,t.Z)("tabs__item",g.tabItem,o?.className,{"tabs__item--active":s===n}),children:i??n},n)}))})}function y(e){let{lazy:n,children:i,selectedValue:t}=e;const o=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===t));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,x.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function j(e){const n=m(e);return(0,x.jsxs)("div",{className:(0,t.Z)("tabs-container",g.tabList),children:[(0,x.jsx)(v,{...n,...e}),(0,x.jsx)(y,{...n,...e})]})}function w(e){const n=(0,f.Z)();return(0,x.jsx)(j,{...e,children:d(e.children)},String(n))}},51102:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/101-way-c2185f0f2f7d884bd0a95f8c37d14b2a.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>r,a:()=>a});var s=i(67294);const t={},o=s.createContext(t);function a(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0eae5577.ca7cde91.js b/assets/js/0eae5577.ca7cde91.js deleted file mode 100644 index fadb4535e..000000000 --- a/assets/js/0eae5577.ca7cde91.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5048],{30433:(e,n,i)=>{i.d(n,{Z:()=>a});i(67294);var s=i(36905);const t={tabItem:"tabItem_Ymn6"};var o=i(85893);function a(e){let{children:n,hidden:i,className:a}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,s.Z)(t.tabItem,a),hidden:i,children:n})}},22808:(e,n,i)=>{i.d(n,{Z:()=>w});var s=i(67294),t=i(36905),o=i(63735),a=i(16550),r=i(20613),c=i(34423),l=i(20636),u=i(99200);function d(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:i}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return d(e).map((e=>{let{props:{value:n,label:i,attributes:s,default:t}}=e;return{value:n,label:i,attributes:s,default:t}}))}(i);return function(e){const n=(0,l.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,i])}function p(e){let{value:n,tabValues:i}=e;return i.some((e=>e.value===n))}function b(e){let{queryString:n=!1,groupId:i}=e;const t=(0,a.k6)(),o=function(e){let{queryString:n=!1,groupId:i}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!i)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return i??null}({queryString:n,groupId:i});return[(0,c._X)(o),(0,s.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(t.location.search);n.set(o,e),t.replace({...t.location,search:n.toString()})}),[o,t])]}function m(e){const{defaultValue:n,queryString:i=!1,groupId:t}=e,o=h(e),[a,c]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:i}=e;if(0===i.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:i}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${i.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=i.find((e=>e.default))??i[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:o}))),[l,d]=b({queryString:i,groupId:t}),[m,f]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,o]=(0,u.Nk)(i);return[t,(0,s.useCallback)((e=>{i&&o.set(e)}),[i,o])]}({groupId:t}),g=(()=>{const e=l??m;return p({value:e,tabValues:o})?e:null})();(0,r.Z)((()=>{g&&c(g)}),[g]);return{selectedValue:a,selectValue:(0,s.useCallback)((e=>{if(!p({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);c(e),d(e),f(e)}),[d,f,o]),tabValues:o}}var f=i(5730);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var x=i(85893);function v(e){let{className:n,block:i,selectedValue:s,selectValue:a,tabValues:r}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),u=e=>{const n=e.currentTarget,i=c.indexOf(n),t=r[i].value;t!==s&&(l(n),a(t))},d=e=>{let n=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const i=c.indexOf(e.currentTarget)+1;n=c[i]??c[0];break}case"ArrowLeft":{const i=c.indexOf(e.currentTarget)-1;n=c[i]??c[c.length-1];break}}n?.focus()};return(0,x.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":i},n),children:r.map((e=>{let{value:n,label:i,attributes:o}=e;return(0,x.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>c.push(e),onKeyDown:d,onClick:u,...o,className:(0,t.Z)("tabs__item",g.tabItem,o?.className,{"tabs__item--active":s===n}),children:i??n},n)}))})}function y(e){let{lazy:n,children:i,selectedValue:t}=e;const o=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===t));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,x.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function j(e){const n=m(e);return(0,x.jsxs)("div",{className:(0,t.Z)("tabs-container",g.tabList),children:[(0,x.jsx)(v,{...n,...e}),(0,x.jsx)(y,{...n,...e})]})}function w(e){const n=(0,f.Z)();return(0,x.jsx)(j,{...e,children:d(e.children)},String(n))}},41183:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>u,contentTitle:()=>c,default:()=>p,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var s=i(85893),t=i(11151),o=i(22808),a=i(30433);const r={title:"101 ways to subscribe user on a personal channel in Centrifugo",tags:["centrifugo","tutorial"],description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/101-way_thumb.jpg",hide_table_of_contents:!1},c=void 0,l={permalink:"/blog/2022/07/29/101-way-to-subscribe",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2022-07-29-101-way-to-subscribe.md",source:"@site/blog/2022-07-29-101-way-to-subscribe.md",title:"101 ways to subscribe user on a personal channel in Centrifugo",description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",date:"2022-07-29T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"tutorial",permalink:"/blog/tags/tutorial"}],readingTime:10.64,hasTruncateMarker:!0,authors:[{name:"Alexander Emelin",title:"Author of Centrifugo",imageURL:"https://github.com/FZambia.png"}],frontMatter:{title:"101 ways to subscribe user on a personal channel in Centrifugo",tags:["centrifugo","tutorial"],description:"In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/101-way_thumb.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",permalink:"/blog/2022/12/20/improving-redis-engine-performance"},nextItem:{title:"Centrifugo v4 released \u2013 a little revolution",permalink:"/blog/2022/07/19/centrifugo-v4-released"}},u={authorsImageUrls:[void 0]},d=[{value:"Setup",id:"setup",level:2},{value:"#1 \u2013 user-limited channel",id:"1--user-limited-channel",level:2},{value:"#2 - channel token authorization",id:"2---channel-token-authorization",level:2},{value:"#3 - subscribe proxy",id:"3---subscribe-proxy",level:2},{value:"#4 - server-side channel in connection JWT",id:"4---server-side-channel-in-connection-jwt",level:2},{value:"#5 - server-side channel in connect proxy",id:"5---server-side-channel-in-connect-proxy",level:2},{value:"#6 - automatic personal channel subscription",id:"6---automatic-personal-channel-subscription",level:2},{value:"#7 \u2013 capabilities in connection JWT",id:"7--capabilities-in-connection-jwt",level:2},{value:"#8 \u2013 capabilities in connect proxy",id:"8--capabilities-in-connect-proxy",level:2},{value:"Teardown",id:"teardown",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifuge",src:i(25583).Z+"",width:"1600",height:"542"})}),"\n",(0,s.jsx)(n.p,{children:"Let's say you develop an application and want a real-time connection which is subscribed to one channel. Let's also assume that this channel is used for user personal notifications. So only one user in the application can subcribe to that channel to receive its notifications in real-time."}),"\n",(0,s.jsx)(n.p,{children:"In this post we will look at various ways to achieve this with Centrifugo, and consider trade-offs of the available approaches. The main goal of this tutorial is to help Centrifugo newcomers be aware of all the ways to control channel permissions by reading just one document."}),"\n",(0,s.jsx)(n.p,{children:"And... well, there are actually 8 ways I found, not 101 \ud83d\ude07"}),"\n",(0,s.jsx)(n.h2,{id:"setup",children:"Setup"}),"\n",(0,s.jsxs)(n.p,{children:["To make the post a bit easier to consume let's setup some things. Let's assume that the user for which we provide all the examples in this post has ID ",(0,s.jsx)(n.code,{children:'"17"'}),". Of course in real-life the examples given here can be extrapolated to any user ID."]}),"\n",(0,s.jsx)(n.p,{children:"When you create a real-time connection to Centrifugo the connection is authenticated using the one of the following ways:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["using ",(0,s.jsx)(n.a,{href:"/docs/server/authentication",children:"connection JWT"})]}),"\n",(0,s.jsxs)(n.li,{children:["using connection request proxy from Centrifugo to the configured endpoint of the application backend (",(0,s.jsx)(n.a,{href:"/docs/server/proxy#connect-proxy",children:"connect proxy"}),")"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"As soon as the connection is successfully established and authenticated Centrifugo knows the ID of connected user. This is important to understand."}),"\n",(0,s.jsx)(n.p,{children:"And let's define a namespace in Centrifugo configuration which will be used for personal user channels:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n ...\n "namespaces": [\n {\n "name": "personal",\n "presence": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Defining namespaces for each new real-time feature is a good practice in Centrifugo. As an awesome improvement we also enabled ",(0,s.jsx)(n.code,{children:"presence"})," in the ",(0,s.jsx)(n.code,{children:"personal"})," namespace, so whenever users subscribe to a channel in this namespace Centrifugo will maintain online presence information for each channel. So you can find out all connections of the specific user existing at any moment. Defining ",(0,s.jsx)(n.code,{children:"presence"})," is fully optional though - turn it of if you don't need presence information and don't want to spend additional server resources on maintaining presence."]}),"\n",(0,s.jsx)(n.h2,{id:"1--user-limited-channel",children:"#1 \u2013 user-limited channel"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most performant approach."})}),"\n",(0,s.jsxs)(n.p,{children:["All you need to do is to extend namespace configuration with ",(0,s.jsx)(n.code,{children:"allow_user_limited_channels"})," option:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "personal",\n "presence": true,\n "allow_user_limited_channels": true\n }\n ]\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"On the client side you need to have sth like this (of course the ID of current user will be dynamic in real-life):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:#17');\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Here you are subscribing to a channel in ",(0,s.jsx)(n.code,{children:"personal"})," namespace and listening to publications coming from a channel. Having ",(0,s.jsx)(n.code,{children:"#"})," in channel name tells Centrifugo that this is a user-limited channel (because ",(0,s.jsx)(n.code,{children:"#"})," is a special symbol that is treated in a special way by Centrifugo as soon as ",(0,s.jsx)(n.code,{children:"allow_user_limited_channels"})," enabled)."]}),"\n",(0,s.jsxs)(n.p,{children:["In this case the user ID part of user-limited channel is ",(0,s.jsx)(n.code,{children:'"17"'}),". So Centrifugo allows user with ID ",(0,s.jsx)(n.code,{children:'"17"'})," to subscribe on ",(0,s.jsx)(n.code,{children:"personal:#17"})," channel. Other users won't be able to subscribe on it."]}),"\n",(0,s.jsxs)(n.p,{children:["To publish updates to subscription all you need to do is to publish to ",(0,s.jsx)(n.code,{children:"personal:#17"})," using server publish API (HTTP or GRPC)."]}),"\n",(0,s.jsx)(n.h2,{id:"2---channel-token-authorization",children:"#2 - channel token authorization"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most flexible approach, with reasonably good performance characteristics."})}),"\n",(0,s.jsx)(n.p,{children:"Another way we will look at is using subscription JWT for subscribing. When you create Subscription object on the client side you can pass it a subscription token, and also provide a function to retrieve subscription token (useful to automatically handle token refresh, it also handles initial token loading)."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const token = await getSubscriptionToken('personal:17');\n\nconst sub = centrifuge.newSubscription('personal:17', {\n token: token\n});\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Inside ",(0,s.jsx)(n.code,{children:"getSubscriptionToken"})," you can issue a request to the backend, for example in browser it's possible to do with fetch API."]}),"\n",(0,s.jsxs)(n.p,{children:["On the backend side you know the ID of current user due to the native session mechanism of your app, so you can decide whether current user has permission to subsribe on ",(0,s.jsx)(n.code,{children:"personal:17"})," or not. If yes \u2013 return subscription JWT according to our rules. If not - return empty string so subscription will go to unsubscribed state with ",(0,s.jsx)(n.code,{children:"unauthorized"})," reason."]}),"\n",(0,s.jsxs)(n.p,{children:["Here are examples for generating subscription HMAC SHA-256 JWTs for channel ",(0,s.jsx)(n.code,{children:"personal:17"})," and HMAC secret key ",(0,s.jsx)(n.code,{children:"secret"}),":"]}),"\n","\n",(0,s.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,s.jsx)(a.Z,{value:"python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {\n "sub": "17",\n "channel": "personal:17"\n "exp": int(time.time()) + 30*60\n}\n\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,s.jsx)(a.Z,{value:"node",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const jose = require('jose')\n\n(async function main() {\n const secret = new TextEncoder().encode('secret')\n const alg = 'HS256'\n\n const token = await new jose.SignJWT({ 'sub': '17', 'channel': 'personal:17' })\n .setProtectedHeader({ alg })\n .setExpirationTime('30m')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Since we set expiration time for subscription JWT tokens we also need to provide a ",(0,s.jsx)(n.code,{children:"getToken"})," function to a client on the frontend side:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:17', {\n getToken: async function (ctx) {\n const token = await getSubscriptionToken('personal:17');\n return token;\n }\n});\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This function will be called by SDK automatically to refresh subscription token when it's going to expire. And note that we omitted setting ",(0,s.jsx)(n.code,{children:"token"})," option here \u2013 since SDK is smart enough to call provided ",(0,s.jsx)(n.code,{children:"getToken"})," function to extract initial subscription token from the backend."]}),"\n",(0,s.jsxs)(n.p,{children:["The good thing in using subscription JWT approach is that you can provide token expiration time, so permissions to subscribe on a channel will be validated from time to time while connection is active. You can also provide additional channel context info which will be attached to presence information (using ",(0,s.jsx)(n.code,{children:"info"})," claim of subscription JWT). And you can granularly control channel permissions using ",(0,s.jsx)(n.code,{children:"allow"})," claim of token \u2013 and give client capabilities to publish, call history or presence information (this is Centrifugo PRO feature at this point). Token also allows to override some namespace options on per-subscription basis (with ",(0,s.jsx)(n.code,{children:"override"})," claim)."]}),"\n",(0,s.jsx)(n.p,{children:"Using subscription tokens is a general approach for any channels where you need to check access first, not only for personal user channels."}),"\n",(0,s.jsx)(n.h2,{id:"3---subscribe-proxy",children:"#3 - subscribe proxy"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Probably the most secure approach."})}),"\n",(0,s.jsx)(n.p,{children:"Subscription JWT gives client a way to subscribe on a channel, and avoid requesting your backend for permission on every resubscribe. Token approach is very good in massive reconnect scenario, when you have many connections and they all resubscribe at once (due to your load balancer reload, for example). But this means that if you unsubscribed client from a channel using server API, client can still resubscribe with token again - until token will expire. In some cases you may want to avoid this."}),"\n",(0,s.jsx)(n.p,{children:"Also, in some cases you want to be notified when someone subscribes to a channel."}),"\n",(0,s.jsx)(n.p,{children:"In this case you may use subscribe proxy feature. When using subscribe proxy every attempt of a client to subscribe on a channel will be translated to request (HTTP or GRPC) from Centrifugo to the application backend. Application backend can decide whether client is allowed to subscribe or not."}),"\n",(0,s.jsxs)(n.p,{children:["One advantage of using subscribe proxy is that backend can additionally provide initial channel data for the subscribing client. This is possible using ",(0,s.jsx)(n.code,{children:"data"})," field of subscribe result generated by backend subscribe handler."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "proxy_subscribe_endpoint": "http://localhost:9000/centrifugo/subscribe",\n "namespaces": [\n {\n "name": "personal",\n "presence": true,\n "proxy_subscribe": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["And on the backend side define a route ",(0,s.jsx)(n.code,{children:"/centrifugo/subscribe"}),", check permissions of user upon subscription and return result to Centrifugo according to our subscribe proxy docs. Or simply run GRPC server using our proxy definitions and react on subscription attempt sent from Centrifugo to backend over GRPC."]}),"\n",(0,s.jsx)(n.p,{children:"On the client-side code is as simple as:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription('personal:17');\nsub.on('publication', function(ctx) {\n console.log(ctx.data);\n})\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"4---server-side-channel-in-connection-jwt",children:"#4 - server-side channel in connection JWT"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"The approach where you don't need to manage client-side subscriptions."})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"/docs/server/server_subs",children:"Server-side subscriptions"})," is a way to consume publications from channels without even create Subscription objects on the client side. In general, client side Subscription objects provide a more flexible and controllable way to work with subscriptions. Clients can subscribe/unsubscribe on channels at any point. Client-side subscriptions provide more details about state transitions."]}),"\n",(0,s.jsx)(n.p,{children:"With server-side subscriptions though you are consuming publications directly from Client instance:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {\n token: 'CONNECTION-JWT'\n});\nclient.on('publication', function(ctx) {\n console.log('publication received from server-side channel', ctx.channel, ctx.data);\n});\nclient.connect();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case you don't have separate Subscription objects and need to look at ",(0,s.jsx)(n.code,{children:"ctx.channel"})," upon receiving publication or to publication content to decide how to handle it. Server-side subscriptions could be a good choice if you are using Centrifugo unidirectional transports and don't need dynamic subscribe/unsubscribe behavior."]}),"\n",(0,s.jsxs)(n.p,{children:["The first way to subscribe client on a server-side channel is to include ",(0,s.jsx)(n.code,{children:"channels"})," claim into connection JWT:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "sub": "17",\n "channels": ["personal:17"]\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"Upon successful connection user will be subscribed to a server-side channel by Centrifugo. One downside of using server-side channels is that errors in one server-side channel (like impossible to recover missed messages) may affect the entire connection and result into reconnects, while with client-side subscriptions individual subsription failures do not affect the entire connection."}),"\n",(0,s.jsx)(n.p,{children:"But having one server-side channel per-connection seems a very reasonable idea to me in many cases. And if you have stable set of subscriptions which do not require lifetime state management \u2013 this can be a nice approach without additional protocol/network overhead involved."}),"\n",(0,s.jsx)(n.h2,{id:"5---server-side-channel-in-connect-proxy",children:"#5 - server-side channel in connect proxy"}),"\n",(0,s.jsx)(n.p,{children:"Similar to the previous one for cases when you are authenticating connections over connect proxy instead of using JWT."}),"\n",(0,s.jsxs)(n.p,{children:["This is possible using ",(0,s.jsx)(n.code,{children:"channels"})," field of connect proxy handler result. The code on the client-side is the same as in Option #4 \u2013 since we only change the way how list of server-side channels is provided."]}),"\n",(0,s.jsx)(n.h2,{id:"6---automatic-personal-channel-subscription",children:"#6 - automatic personal channel subscription"}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Almost no code approach."})}),"\n",(0,s.jsx)(n.p,{children:"As we pointed above Centrifugo knows an ID of the user due to authentication process. So why not combining this knowledge with automatic server-side personal channel subscription? Centrifugo provides exactly this with user personal channel feature."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "user_subscribe_to_personal": true,\n "user_personal_channel_namespace": "personal",\n "namespaces": [\n {\n "name": "personal",\n "presence": true\n }\n ]\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This feature only subscribes non-anonymous users to personal channels (those with non-empty user ID). The configuration above will subscribe our user ",(0,s.jsx)(n.code,{children:'"17"'})," to channel ",(0,s.jsx)(n.code,{children:"personal:#17"})," automatically after successful authentication."]}),"\n",(0,s.jsx)(n.h2,{id:"7--capabilities-in-connection-jwt",children:"#7 \u2013 capabilities in connection JWT"}),"\n",(0,s.jsx)(n.p,{children:"Allows using client-side subscriptions, but skip receiving subscription token. This is only available in Centrifugo PRO at this point."}),"\n",(0,s.jsxs)(n.p,{children:["So when generating JWT you can provide additional ",(0,s.jsx)(n.code,{children:"caps"})," claim which contains channel resource capabilities:"]}),"\n",(0,s.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,s.jsx)(a.Z,{value:"python",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {\n "sub": "17",\n "exp": int(time.time()) + 30*60,\n "caps": [\n {\n "channels": ["personal:17"],\n "allow": ["sub"]\n }\n ]\n}\n\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,s.jsx)(a.Z,{value:"node",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const jose = require('jose');\n\n(async function main() {\n const secret = new TextEncoder().encode('secret')\n const alg = 'HS256'\n\n const token = await new jose.SignJWT({\n sub: '17',\n caps: [\n {\n \"channels\": [\"personal:17\"],\n \"allow\": [\"sub\"]\n }\n ]\n })\n .setProtectedHeader({ alg })\n .setExpirationTime('30m')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,s.jsx)(n.p,{children:"While in case of single channel the benefit of using this approach is not really obvious, it can help when you are using several channels with stric access permissions per connection, where providing capabilities can help to save some traffic and CPU resources since we avoid generating subscription token for each individual channel."}),"\n",(0,s.jsx)(n.h2,{id:"8--capabilities-in-connect-proxy",children:"#8 \u2013 capabilities in connect proxy"}),"\n",(0,s.jsx)(n.p,{children:"This is very similar to the previous approach, but capabilities are passed to Centrifugo in connect proxy result. So if you are using connect proxy for auth then you can still provide capabilities in the same form as in JWT. This is also a Centrifugo PRO feature."}),"\n",(0,s.jsx)(n.h2,{id:"teardown",children:"Teardown"}),"\n",(0,s.jsx)(n.p,{children:"Which way to choose? Well, it depends. Since your application will have more than only a personal user channel in many cases you should decide which approach suits you better in each particular case \u2013 it's hard to give the universal advice."}),"\n",(0,s.jsx)(n.p,{children:"Client-side subscriptions are more flexible in general, so I'd suggest using them whenever possible. Though you may use unidirectional transports of Centrifugo where subscribing to channels from the client side is not simple to achieve (though still possible using our server subscribe API). Server-side subscriptions make more sense there."}),"\n",(0,s.jsx)(n.p,{children:"The good news is that all our official bidirectional client SDKs support all the approaches mentioned in this post. Hope designing the channel configuration on top of Centrifugo will be a pleasant experience for you."}),"\n",(0,s.jsx)(n.admonition,{title:"Attributions",type:"note",children:(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)("a",{href:"https://www.freepik.com/vectors/internet-network",children:["Internet network vector created by rawpixel.com - ",(0,s.jsx)(n.a,{href:"http://www.freepik.com",children:"www.freepik.com"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)("a",{href:"https://www.flaticon.com/free-icons/cyber-security",title:"cyber security icons",children:"Cyber security icons created by Smashicons - Flaticon"}),"\n"]}),"\n"]})})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},25583:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/101-way-c2185f0f2f7d884bd0a95f8c37d14b2a.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>r,a:()=>a});var s=i(67294);const t={},o=s.createContext(t);function a(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1248e41e.566dbeb1.js b/assets/js/1248e41e.566dbeb1.js deleted file mode 100644 index e5625cb39..000000000 --- a/assets/js/1248e41e.566dbeb1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3672],{26314:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=i(85893),s=i(11151);const r={id:"analytics",title:"Analytics with ClickHouse"},o=void 0,c={id:"pro/analytics",title:"Analytics with ClickHouse",description:"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.",source:"@site/docs/pro/analytics.md",sourceDirName:"pro",slug:"/pro/analytics",permalink:"/docs/pro/analytics",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/analytics.md",tags:[],version:"current",frontMatter:{id:"analytics",title:"Analytics with ClickHouse"},sidebar:"Pro",previous:{title:"User and channel tracing",permalink:"/docs/pro/tracing"},next:{title:"Operation rate limits",permalink:"/docs/pro/rate_limiting"}},a={},l=[{value:"Configuration",id:"configuration",level:2},{value:"Connections table",id:"connections-table",level:2},{value:"Subscriptions table",id:"subscriptions-table",level:2},{value:"Operations table",id:"operations-table",level:2},{value:"Publications table",id:"publications-table",level:2},{value:"Notifications table",id:"notifications-table",level:2},{value:"Query examples",id:"query-examples",level:2},{value:"Development",id:"development",level:2},{value:"How export works",id:"how-export-works",level:2},{value:"Exposed metrics",id:"exposed-metrics",level:2},{value:"centrifugo_clickhouse_analytics_drop_count",id:"centrifugo_clickhouse_analytics_drop_count",level:4},{value:"centrifugo_clickhouse_analytics_flush_duration_seconds",id:"centrifugo_clickhouse_analytics_flush_duration_seconds",level:4},{value:"centrifugo_clickhouse_analytics_batch_size",id:"centrifugo_clickhouse_analytics_batch_size",level:4}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ",(0,t.jsx)(n.a,{href:"https://clickhouse.com/",children:"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."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"clickhouse",src:i(14323).Z+"",width:"3346",height:"1067"})}),"\n",(0,t.jsx)(n.p,{children:"This unlocks a great observability and a way to perform various analytics queries for better connection behavior understanding, check application correctness, building trends, reports, and so on."}),"\n",(0,t.jsx)(n.p,{children:"As soon as you start using integration with ClickHouse some of mentioned possibilities may be easily accessed with Centrifugo PRO web UI and it's analytics page:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Admin analytics",src:i(37508).Z+"",width:"2854",height:"1396"})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n.p,{children:"To enable integration with ClickHouse add the following section to a configuration file:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000",\n "tcp://127.0.0.1:9001",\n "tcp://127.0.0.1:9002",\n "tcp://127.0.0.1:9003"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "centrifugo_cluster",\n "export_connections": true,\n "export_subscriptions": true,\n "export_operations": true,\n "export_publications": true,\n "export_notifications": true,\n "export_http_headers": [\n "User-Agent",\n "Origin",\n "X-Real-Ip"\n ]\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All ClickHouse analytics options scoped to ",(0,t.jsx)(n.code,{children:"clickhouse_analytics"})," section of configuration."]}),"\n",(0,t.jsxs)(n.p,{children:["Toggle this feature using ",(0,t.jsx)(n.code,{children:"enabled"})," boolean option."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["While we have a nested configuration here it's still possible to use environment variables to set options. For example, use ",(0,t.jsx)(n.code,{children:"CENTRIFUGO_CLICKHOUSE_ANALYTICS_ENABLED"})," env var name for configure ",(0,t.jsx)(n.code,{children:"enabled"})," option mentioned above. I.e. nesting expressed as ",(0,t.jsx)(n.code,{children:"_"})," in Centrifugo."]})}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo can export data to different ClickHouse instances, addresses of ClickHouse can be set over ",(0,t.jsx)(n.code,{children:"clickhouse_dsn"})," option."]}),"\n",(0,t.jsxs)(n.p,{children:["You also need to set a ClickHouse cluster name (",(0,t.jsx)(n.code,{children:"clickhouse_cluster"}),") and database name ",(0,t.jsx)(n.code,{children:"clickhouse_database"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_connections"})," tells Centrifugo to export connection information snapshots. Information about connection will be exported once a connection established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_subscriptions"})," tells Centrifugo to export subscription information snapshots. Information about subscription will be exported once a subscription established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_operations"})," tells Centrifugo to export individual client operation information. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_publications"})," tells Centrifugo to export publications for channels to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_notifications"})," tells Centrifugo to export push notifications to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_http_headers"})," is a list of HTTP headers to export for connection information."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_grpc_metadata"})," is a list of metadata keys to export for connection information for GRPC unidirectional transport."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_schema_initialization"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". By default Centrifugo tries to initialize table schema on start (if not exists). This flag allows skipping initialization process."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_ping_on_start"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". Centrifugo pings Clickhouse servers by default on start, if any of servers is unavailable \u2013 Centrifugo fails to start. This option allow skipping this check thus Centrifugo is able to start even if Clickhouse cluster not working correctly."]}),"\n",(0,t.jsx)(n.h2,{id:"connections-table",children:"Connections table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/connections', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections_distributed\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'connections', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"subscriptions-table",children:"Subscriptions table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions_distributed\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'subscriptions', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"operations-table",children:"Operations table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/operations', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'operations', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"publications-table",children:"Publications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.publications\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'publications', murmurHash3_64(channel)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"notifications-table",children:"Notifications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.notifications\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'notifications', murmurHash3_64(uid)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"query-examples",children:"Query examples"}),"\n",(0,t.jsx)(n.p,{children:"Show unique users which were connected:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT DISTINCT user\nFROM centrifugo.connections_distributed;\n\n\u250c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 user_1 \u2502\n\u2502 user_2 \u2502\n\u2502 user_3 \u2502\n\u2502 user_4 \u2502\n\u2502 user_5 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show total number of publication attempts which were throttled by Centrifugo (received ",(0,t.jsx)(n.code,{children:"Too many requests"})," error with code ",(0,t.jsx)(n.code,{children:"111"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 4502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"The same for a specific user:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish') AND (user = 'user_200');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 1214 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show number of unique users subscribed to a specific channel in last 5 minutes (this is approximate since subscriptions table contain periodic snapshot entries, clients could unsubscribe in between snapshots \u2013 this is reflected in operations table):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(Distinct(user))\nFROM centrifugo.subscriptions_distributed\nWHERE arrayExists(x -> (x = 'chat:index'), channels) AND (time >= (now() - toIntervalMinute(5)));\n\n\u250c\u2500uniqExact(user)\u2500\u2510\n\u2502 101 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show top 10 users which called ",(0,t.jsx)(n.code,{children:"publish"})," operation during last one minute:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT\n COUNT(op) AS num_ops,\n user\nFROM centrifugo.operations_distributed\nWHERE (op = 'publish') AND (time >= (now() - toIntervalMinute(1)))\nGROUP BY user\nORDER BY num_ops DESC\nLIMIT 10;\n\n\u250c\u2500num_ops\u2500\u252c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 56 \u2502 user_200 \u2502\n\u2502 11 \u2502 user_75 \u2502\n\u2502 6 \u2502 user_87 \u2502\n\u2502 6 \u2502 user_65 \u2502\n\u2502 6 \u2502 user_39 \u2502\n\u2502 5 \u2502 user_28 \u2502\n\u2502 5 \u2502 user_63 \u2502\n\u2502 5 \u2502 user_89 \u2502\n\u2502 3 \u2502 user_32 \u2502\n\u2502 3 \u2502 user_52 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show total number of push notifications to iOS devices sent during last 24 hours:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.notifications\nWHERE (time > (now() - toIntervalHour(24))) AND (platform = 'ios')\n\n\u250c\u2500count()\u2500\u2510\n\u2502 31200 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"development",children:"Development"}),"\n",(0,t.jsxs)(n.p,{children:["The recommended way to run ClickHouse in production is with cluster. See ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/tree/master/misc/clickhouse_cluster",children:"an example of such cluster configuration"})," made with Docker Compose."]}),"\n",(0,t.jsx)(n.p,{children:"But during development you may want to run Centrifugo with single instance ClickHouse."}),"\n",(0,t.jsx)(n.p,{children:"To do this set only one ClickHouse dsn and do not set cluster name:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "",\n "export_connections": true,\n "export_subscriptions": true,\n "export_publications": true,\n "export_operations": true,\n "export_http_headers": [\n "Origin",\n "User-Agent"\n ]\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse locally:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm -v /tmp/clickhouse:/var/lib/clickhouse -p 9000:9000 --name click clickhouse/clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse client:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm --link click:clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server --host clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Issue queries:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:":) SELECT * FROM centrifugo.operations\n\n\u250c\u2500client\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500user\u2500\u252c\u2500op\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500channel\u2500\u2500\u2500\u2500\u2500\u252c\u2500method\u2500\u252c\u2500error\u2500\u252c\u2500disconnect\u2500\u252c\u2500duration\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500time\u2500\u2510\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connecting \u2502 \u2502 \u2502 0 \u2502 0 \u2502 217894 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connect \u2502 \u2502 \u2502 0 \u2502 0 \u2502 0 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 92714 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 presence \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 3539 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test1 \u2502 \u2502 0 \u2502 0 \u2502 2402 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test2 \u2502 \u2502 0 \u2502 0 \u2502 634 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test3 \u2502 \u2502 0 \u2502 0 \u2502 412 \u2502 2021-07-31 08:15:12 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"how-export-works",children:"How export works"}),"\n",(0,t.jsx)(n.p,{children:"When ClickHouse analytics enabled Centrifugo nodes start exporting events to ClickHouse. Each node issues insert with events once in 10 seconds (flushing collected events in batches thus making insertion in ClickHouse efficient). Maximum batch size is 100k for each table at the momemt. If insert to ClickHouse failed Centrifugo retries it once and then buffers events in memory (up to 1 million entries). If ClickHouse still unavailable after collecting 1 million events then new events will be dropped until buffer has space. These limits are configurable. Centrifugo PRO uses very efficient code for writing data to ClickHouse, so analytics feature should only add a little overhead for Centrifugo node."}),"\n",(0,t.jsx)(n.h2,{id:"exposed-metrics",children:"Exposed metrics"}),"\n",(0,t.jsx)(n.p,{children:"Several metrics are exposed to monitor export process health:"}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_drop_count",children:"centrifugo_clickhouse_analytics_drop_count"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Counter"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Total count of drops."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Useful for tracking the number of data drops in ClickHouse analytics, helping identify potential issues with data processing."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_flush_duration_seconds",children:"centrifugo_clickhouse_analytics_flush_duration_seconds"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type, retries, result"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Duration of ClickHouse data flush in seconds."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Helps in monitoring the performance of data flush operations in ClickHouse, aiding in performance tuning and issue resolution."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_batch_size",children:"centrifugo_clickhouse_analytics_batch_size"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Distribution of batch sizes for ClickHouse flush."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Useful for understanding the size of data batches being flushed to ClickHouse, helping optimize performance."]}),"\n"]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},14323:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/clickhouse-9b4cfb14c477ffac11caf495a679bd94.png"},37508:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/pro_analytics-b47bb94134e4da79361e6c901f6917f7.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>o});var t=i(67294);const s={},r=t.createContext(s);function o(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1248e41e.8effc9e6.js b/assets/js/1248e41e.8effc9e6.js new file mode 100644 index 000000000..f65630f09 --- /dev/null +++ b/assets/js/1248e41e.8effc9e6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3672],{26314:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=i(85893),s=i(11151);const r={id:"analytics",title:"Analytics with ClickHouse"},o=void 0,c={id:"pro/analytics",title:"Analytics with ClickHouse",description:"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.",source:"@site/docs/pro/analytics.md",sourceDirName:"pro",slug:"/pro/analytics",permalink:"/docs/pro/analytics",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/analytics.md",tags:[],version:"current",frontMatter:{id:"analytics",title:"Analytics with ClickHouse"},sidebar:"Pro",previous:{title:"User and channel tracing",permalink:"/docs/pro/tracing"},next:{title:"Operation rate limits",permalink:"/docs/pro/rate_limiting"}},a={},l=[{value:"Configuration",id:"configuration",level:2},{value:"Connections table",id:"connections-table",level:2},{value:"Subscriptions table",id:"subscriptions-table",level:2},{value:"Operations table",id:"operations-table",level:2},{value:"Publications table",id:"publications-table",level:2},{value:"Notifications table",id:"notifications-table",level:2},{value:"Query examples",id:"query-examples",level:2},{value:"Development",id:"development",level:2},{value:"How export works",id:"how-export-works",level:2},{value:"Exposed metrics",id:"exposed-metrics",level:2},{value:"centrifugo_clickhouse_analytics_drop_count",id:"centrifugo_clickhouse_analytics_drop_count",level:4},{value:"centrifugo_clickhouse_analytics_flush_duration_seconds",id:"centrifugo_clickhouse_analytics_flush_duration_seconds",level:4},{value:"centrifugo_clickhouse_analytics_batch_size",id:"centrifugo_clickhouse_analytics_batch_size",level:4}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ",(0,t.jsx)(n.a,{href:"https://clickhouse.com/",children:"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."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"clickhouse",src:i(50895).Z+"",width:"3346",height:"1067"})}),"\n",(0,t.jsx)(n.p,{children:"This unlocks a great observability and a way to perform various analytics queries for better connection behavior understanding, check application correctness, building trends, reports, and so on."}),"\n",(0,t.jsx)(n.p,{children:"As soon as you start using integration with ClickHouse some of mentioned possibilities may be easily accessed with Centrifugo PRO web UI and it's analytics page:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Admin analytics",src:i(4862).Z+"",width:"2854",height:"1396"})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n.p,{children:"To enable integration with ClickHouse add the following section to a configuration file:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000",\n "tcp://127.0.0.1:9001",\n "tcp://127.0.0.1:9002",\n "tcp://127.0.0.1:9003"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "centrifugo_cluster",\n "export_connections": true,\n "export_subscriptions": true,\n "export_operations": true,\n "export_publications": true,\n "export_notifications": true,\n "export_http_headers": [\n "User-Agent",\n "Origin",\n "X-Real-Ip"\n ]\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All ClickHouse analytics options scoped to ",(0,t.jsx)(n.code,{children:"clickhouse_analytics"})," section of configuration."]}),"\n",(0,t.jsxs)(n.p,{children:["Toggle this feature using ",(0,t.jsx)(n.code,{children:"enabled"})," boolean option."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["While we have a nested configuration here it's still possible to use environment variables to set options. For example, use ",(0,t.jsx)(n.code,{children:"CENTRIFUGO_CLICKHOUSE_ANALYTICS_ENABLED"})," env var name for configure ",(0,t.jsx)(n.code,{children:"enabled"})," option mentioned above. I.e. nesting expressed as ",(0,t.jsx)(n.code,{children:"_"})," in Centrifugo."]})}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo can export data to different ClickHouse instances, addresses of ClickHouse can be set over ",(0,t.jsx)(n.code,{children:"clickhouse_dsn"})," option."]}),"\n",(0,t.jsxs)(n.p,{children:["You also need to set a ClickHouse cluster name (",(0,t.jsx)(n.code,{children:"clickhouse_cluster"}),") and database name ",(0,t.jsx)(n.code,{children:"clickhouse_database"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_connections"})," tells Centrifugo to export connection information snapshots. Information about connection will be exported once a connection established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_subscriptions"})," tells Centrifugo to export subscription information snapshots. Information about subscription will be exported once a subscription established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_operations"})," tells Centrifugo to export individual client operation information. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_publications"})," tells Centrifugo to export publications for channels to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_notifications"})," tells Centrifugo to export push notifications to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_http_headers"})," is a list of HTTP headers to export for connection information."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_grpc_metadata"})," is a list of metadata keys to export for connection information for GRPC unidirectional transport."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_schema_initialization"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". By default Centrifugo tries to initialize table schema on start (if not exists). This flag allows skipping initialization process."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_ping_on_start"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". Centrifugo pings Clickhouse servers by default on start, if any of servers is unavailable \u2013 Centrifugo fails to start. This option allow skipping this check thus Centrifugo is able to start even if Clickhouse cluster not working correctly."]}),"\n",(0,t.jsx)(n.h2,{id:"connections-table",children:"Connections table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/connections', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections_distributed\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'connections', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"subscriptions-table",children:"Subscriptions table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions_distributed\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'subscriptions', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"operations-table",children:"Operations table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/operations', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'operations', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"publications-table",children:"Publications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.publications\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'publications', murmurHash3_64(channel)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"notifications-table",children:"Notifications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.notifications\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'notifications', murmurHash3_64(uid)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"query-examples",children:"Query examples"}),"\n",(0,t.jsx)(n.p,{children:"Show unique users which were connected:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT DISTINCT user\nFROM centrifugo.connections_distributed;\n\n\u250c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 user_1 \u2502\n\u2502 user_2 \u2502\n\u2502 user_3 \u2502\n\u2502 user_4 \u2502\n\u2502 user_5 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show total number of publication attempts which were throttled by Centrifugo (received ",(0,t.jsx)(n.code,{children:"Too many requests"})," error with code ",(0,t.jsx)(n.code,{children:"111"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 4502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"The same for a specific user:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish') AND (user = 'user_200');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 1214 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show number of unique users subscribed to a specific channel in last 5 minutes (this is approximate since subscriptions table contain periodic snapshot entries, clients could unsubscribe in between snapshots \u2013 this is reflected in operations table):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(Distinct(user))\nFROM centrifugo.subscriptions_distributed\nWHERE arrayExists(x -> (x = 'chat:index'), channels) AND (time >= (now() - toIntervalMinute(5)));\n\n\u250c\u2500uniqExact(user)\u2500\u2510\n\u2502 101 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show top 10 users which called ",(0,t.jsx)(n.code,{children:"publish"})," operation during last one minute:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT\n COUNT(op) AS num_ops,\n user\nFROM centrifugo.operations_distributed\nWHERE (op = 'publish') AND (time >= (now() - toIntervalMinute(1)))\nGROUP BY user\nORDER BY num_ops DESC\nLIMIT 10;\n\n\u250c\u2500num_ops\u2500\u252c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 56 \u2502 user_200 \u2502\n\u2502 11 \u2502 user_75 \u2502\n\u2502 6 \u2502 user_87 \u2502\n\u2502 6 \u2502 user_65 \u2502\n\u2502 6 \u2502 user_39 \u2502\n\u2502 5 \u2502 user_28 \u2502\n\u2502 5 \u2502 user_63 \u2502\n\u2502 5 \u2502 user_89 \u2502\n\u2502 3 \u2502 user_32 \u2502\n\u2502 3 \u2502 user_52 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show total number of push notifications to iOS devices sent during last 24 hours:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.notifications\nWHERE (time > (now() - toIntervalHour(24))) AND (platform = 'ios')\n\n\u250c\u2500count()\u2500\u2510\n\u2502 31200 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"development",children:"Development"}),"\n",(0,t.jsxs)(n.p,{children:["The recommended way to run ClickHouse in production is with cluster. See ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/tree/master/misc/clickhouse_cluster",children:"an example of such cluster configuration"})," made with Docker Compose."]}),"\n",(0,t.jsx)(n.p,{children:"But during development you may want to run Centrifugo with single instance ClickHouse."}),"\n",(0,t.jsx)(n.p,{children:"To do this set only one ClickHouse dsn and do not set cluster name:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "",\n "export_connections": true,\n "export_subscriptions": true,\n "export_publications": true,\n "export_operations": true,\n "export_http_headers": [\n "Origin",\n "User-Agent"\n ]\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse locally:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm -v /tmp/clickhouse:/var/lib/clickhouse -p 9000:9000 --name click clickhouse/clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse client:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm --link click:clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server --host clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Issue queries:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:":) SELECT * FROM centrifugo.operations\n\n\u250c\u2500client\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500user\u2500\u252c\u2500op\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500channel\u2500\u2500\u2500\u2500\u2500\u252c\u2500method\u2500\u252c\u2500error\u2500\u252c\u2500disconnect\u2500\u252c\u2500duration\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500time\u2500\u2510\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connecting \u2502 \u2502 \u2502 0 \u2502 0 \u2502 217894 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connect \u2502 \u2502 \u2502 0 \u2502 0 \u2502 0 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 92714 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 presence \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 3539 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test1 \u2502 \u2502 0 \u2502 0 \u2502 2402 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test2 \u2502 \u2502 0 \u2502 0 \u2502 634 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test3 \u2502 \u2502 0 \u2502 0 \u2502 412 \u2502 2021-07-31 08:15:12 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"how-export-works",children:"How export works"}),"\n",(0,t.jsx)(n.p,{children:"When ClickHouse analytics enabled Centrifugo nodes start exporting events to ClickHouse. Each node issues insert with events once in 10 seconds (flushing collected events in batches thus making insertion in ClickHouse efficient). Maximum batch size is 100k for each table at the momemt. If insert to ClickHouse failed Centrifugo retries it once and then buffers events in memory (up to 1 million entries). If ClickHouse still unavailable after collecting 1 million events then new events will be dropped until buffer has space. These limits are configurable. Centrifugo PRO uses very efficient code for writing data to ClickHouse, so analytics feature should only add a little overhead for Centrifugo node."}),"\n",(0,t.jsx)(n.h2,{id:"exposed-metrics",children:"Exposed metrics"}),"\n",(0,t.jsx)(n.p,{children:"Several metrics are exposed to monitor export process health:"}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_drop_count",children:"centrifugo_clickhouse_analytics_drop_count"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Counter"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Total count of drops."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Useful for tracking the number of data drops in ClickHouse analytics, helping identify potential issues with data processing."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_flush_duration_seconds",children:"centrifugo_clickhouse_analytics_flush_duration_seconds"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type, retries, result"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Duration of ClickHouse data flush in seconds."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Helps in monitoring the performance of data flush operations in ClickHouse, aiding in performance tuning and issue resolution."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"centrifugo_clickhouse_analytics_batch_size",children:"centrifugo_clickhouse_analytics_batch_size"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Labels:"})," type"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Description:"})," Distribution of batch sizes for ClickHouse flush."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Usage:"})," Useful for understanding the size of data batches being flushed to ClickHouse, helping optimize performance."]}),"\n"]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},50895:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/clickhouse-9b4cfb14c477ffac11caf495a679bd94.png"},4862:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/pro_analytics-b47bb94134e4da79361e6c901f6917f7.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>o});var t=i(67294);const s={},r=t.createContext(s);function o(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1343.39ce3873.js b/assets/js/1343.39ce3873.js deleted file mode 100644 index 3d5194153..000000000 --- a/assets/js/1343.39ce3873.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1343],{84316:(e,t,n)=>{"use strict";n.d(t,{Z:()=>V});var o=n(67294),s=n(5730),c=n(36905),r=n(70524),a=n(96793);function l(){const{prism:e}=(0,a.L)(),{colorMode:t}=(0,r.I)(),n=e.theme,o=e.darkTheme||n;return"dark"===t?o:n}var i=n(18015),u=n(87594),d=n.n(u);const m=/title=(?["'])(?.*?)\1/,p=/\{(?<range>[\d,-]+)\}/,b={js:{start:"\\/\\/",end:""},jsBlock:{start:"\\/\\*",end:"\\*\\/"},jsx:{start:"\\{\\s*\\/\\*",end:"\\*\\/\\s*\\}"},bash:{start:"#",end:""},html:{start:"\x3c!--",end:"--\x3e"}},f={...b,lua:{start:"--",end:""},wasm:{start:"\\;\\;",end:""},tex:{start:"%",end:""},vb:{start:"['\u2018\u2019]",end:""},vbnet:{start:"(?:_\\s*)?['\u2018\u2019]",end:""},rem:{start:"[Rr][Ee][Mm]\\b",end:""},f90:{start:"!",end:""},ml:{start:"\\(\\*",end:"\\*\\)"},cobol:{start:"\\*>",end:""}},h=Object.keys(b);function g(e,t){const n=e.map((e=>{const{start:n,end:o}=f[e];return`(?:${n}\\s*(${t.flatMap((e=>[e.line,e.block?.start,e.block?.end].filter(Boolean))).join("|")})\\s*${o})`})).join("|");return new RegExp(`^\\s*(?:${n})\\s*$`)}function k(e,t){let n=e.replace(/\n$/,"");const{language:o,magicComments:s,metastring:c}=t;if(c&&p.test(c)){const e=c.match(p).groups.range;if(0===s.length)throw new Error(`A highlight range has been given in code block's metastring (\`\`\` ${c}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.`);const t=s[0].className,o=d()(e).filter((e=>e>0)).map((e=>[e-1,[t]]));return{lineClassNames:Object.fromEntries(o),code:n}}if(void 0===o)return{lineClassNames:{},code:n};const r=function(e,t){switch(e){case"js":case"javascript":case"ts":case"typescript":return g(["js","jsBlock"],t);case"jsx":case"tsx":return g(["js","jsBlock","jsx"],t);case"html":return g(["js","jsBlock","html"],t);case"python":case"py":case"bash":return g(["bash"],t);case"markdown":case"md":return g(["html","jsx","bash"],t);case"tex":case"latex":case"matlab":return g(["tex"],t);case"lua":case"haskell":case"sql":return g(["lua"],t);case"wasm":return g(["wasm"],t);case"vb":case"vba":case"visual-basic":return g(["vb","rem"],t);case"vbnet":return g(["vbnet","rem"],t);case"batch":return g(["rem"],t);case"basic":return g(["rem","f90"],t);case"fsharp":return g(["js","ml"],t);case"ocaml":case"sml":return g(["ml"],t);case"fortran":return g(["f90"],t);case"cobol":return g(["cobol"],t);default:return g(h,t)}}(o,s),a=n.split("\n"),l=Object.fromEntries(s.map((e=>[e.className,{start:0,range:""}]))),i=Object.fromEntries(s.filter((e=>e.line)).map((e=>{let{className:t,line:n}=e;return[n,t]}))),u=Object.fromEntries(s.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.start,t]}))),m=Object.fromEntries(s.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.end,t]})));for(let d=0;d<a.length;){const e=a[d].match(r);if(!e){d+=1;continue}const t=e.slice(1).find((e=>void 0!==e));i[t]?l[i[t]].range+=`${d},`:u[t]?l[u[t]].start=d:m[t]&&(l[m[t]].range+=`${l[m[t]].start}-${d-1},`),a.splice(d,1)}n=a.join("\n");const b={};return Object.entries(l).forEach((e=>{let[t,{range:n}]=e;d()(n).forEach((e=>{b[e]??=[],b[e].push(t)}))})),{lineClassNames:b,code:n}}const x={codeBlockContainer:"codeBlockContainer_Ckt0"};var B=n(85893);function j(e){let{as:t,...n}=e;const o=function(e){const t={color:"--prism-color",backgroundColor:"--prism-background-color"},n={};return Object.entries(e.plain).forEach((e=>{let[o,s]=e;const c=t[o];c&&"string"==typeof s&&(n[c]=s)})),n}(l());return(0,B.jsx)(t,{...n,style:o,className:(0,c.Z)(n.className,x.codeBlockContainer,i.k.common.codeBlock)})}const v={codeBlockContent:"codeBlockContent_biex",codeBlockTitle:"codeBlockTitle_Ktv7",codeBlock:"codeBlock_bY9V",codeBlockStandalone:"codeBlockStandalone_MEMb",codeBlockLines:"codeBlockLines_e6Vv",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_o6Pm",buttonGroup:"buttonGroup__atx"};function y(e){let{children:t,className:n}=e;return(0,B.jsx)(j,{as:"pre",tabIndex:0,className:(0,c.Z)(v.codeBlockStandalone,"thin-scrollbar",n),children:(0,B.jsx)("code",{className:v.codeBlockLines,children:t})})}var C=n(93478);const N={attributes:!0,characterData:!0,childList:!0,subtree:!0};function w(e,t){const[n,s]=(0,o.useState)(),c=(0,o.useCallback)((()=>{s(e.current?.closest("[role=tabpanel][hidden]"))}),[e,s]);(0,o.useEffect)((()=>{c()}),[c]),function(e,t,n){void 0===n&&(n=N);const s=(0,C.zX)(t),c=(0,C.Ql)(n);(0,o.useEffect)((()=>{const t=new MutationObserver(s);return e&&t.observe(e,c),()=>t.disconnect()}),[e,s,c])}(n,(e=>{e.forEach((e=>{"attributes"===e.type&&"hidden"===e.attributeName&&(t(),c())}))}),{attributes:!0,characterData:!1,childList:!1,subtree:!1})}var L=n(14965);const E={codeLine:"codeLine_lJS_",codeLineNumber:"codeLineNumber_Tfdd",codeLineContent:"codeLineContent_feaV"};function I(e){let{line:t,classNames:n,showLineNumbers:o,getLineProps:s,getTokenProps:r}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const a=s({line:t,className:(0,c.Z)(n,o&&E.codeLine)}),l=t.map(((e,t)=>(0,B.jsx)("span",{...r({token:e})},t)));return(0,B.jsxs)("span",{...a,children:[o?(0,B.jsxs)(B.Fragment,{children:[(0,B.jsx)("span",{className:E.codeLineNumber}),(0,B.jsx)("span",{className:E.codeLineContent,children:l})]}):l,(0,B.jsx)("br",{})]})}var S=n(11614);function _(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"})})}function A(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"})})}const T={copyButtonCopied:"copyButtonCopied_obH4",copyButtonIcons:"copyButtonIcons_eSgA",copyButtonIcon:"copyButtonIcon_y97N",copyButtonSuccessIcon:"copyButtonSuccessIcon_LjdS"};function $(e){let{code:t,className:n}=e;const[s,r]=(0,o.useState)(!1),a=(0,o.useRef)(void 0),l=(0,o.useCallback)((()=>{!function(e,t){let{target:n=document.body}=void 0===t?{}:t;if("string"!=typeof e)throw new TypeError(`Expected parameter \`text\` to be a \`string\`, got \`${typeof e}\`.`);const o=document.createElement("textarea"),s=document.activeElement;o.value=e,o.setAttribute("readonly",""),o.style.contain="strict",o.style.position="absolute",o.style.left="-9999px",o.style.fontSize="12pt";const c=document.getSelection(),r=c.rangeCount>0&&c.getRangeAt(0);n.append(o),o.select(),o.selectionStart=0,o.selectionEnd=e.length;let a=!1;try{a=document.execCommand("copy")}catch{}o.remove(),r&&(c.removeAllRanges(),c.addRange(r)),s&&s.focus()}(t),r(!0),a.current=window.setTimeout((()=>{r(!1)}),1e3)}),[t]);return(0,o.useEffect)((()=>()=>window.clearTimeout(a.current)),[]),(0,B.jsx)("button",{type:"button","aria-label":s?(0,S.I)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,S.I)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,S.I)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,c.Z)("clean-btn",n,T.copyButton,s&&T.copyButtonCopied),onClick:l,children:(0,B.jsxs)("span",{className:T.copyButtonIcons,"aria-hidden":"true",children:[(0,B.jsx)(_,{className:T.copyButtonIcon}),(0,B.jsx)(A,{className:T.copyButtonSuccessIcon})]})})}function W(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"})})}const M={wordWrapButtonIcon:"wordWrapButtonIcon_Bwma",wordWrapButtonEnabled:"wordWrapButtonEnabled_EoeP"};function Z(e){let{className:t,onClick:n,isEnabled:o}=e;const s=(0,S.I)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return(0,B.jsx)("button",{type:"button",onClick:n,className:(0,c.Z)("clean-btn",t,o&&M.wordWrapButtonEnabled),"aria-label":s,title:s,children:(0,B.jsx)(W,{className:M.wordWrapButtonIcon,"aria-hidden":"true"})})}function H(e){let{children:t,className:n="",metastring:s,title:r,showLineNumbers:i,language:u}=e;const{prism:{defaultLanguage:d,magicComments:p}}=(0,a.L)(),b=function(e){return e?.toLowerCase()}(u??function(e){const t=e.split(" ").find((e=>e.startsWith("language-")));return t?.replace(/language-/,"")}(n)??d),f=l(),h=function(){const[e,t]=(0,o.useState)(!1),[n,s]=(0,o.useState)(!1),c=(0,o.useRef)(null),r=(0,o.useCallback)((()=>{const n=c.current.querySelector("code");e?n.removeAttribute("style"):(n.style.whiteSpace="pre-wrap",n.style.overflowWrap="anywhere"),t((e=>!e))}),[c,e]),a=(0,o.useCallback)((()=>{const{scrollWidth:e,clientWidth:t}=c.current,n=e>t||c.current.querySelector("code").hasAttribute("style");s(n)}),[c]);return w(c,a),(0,o.useEffect)((()=>{a()}),[e,a]),(0,o.useEffect)((()=>(window.addEventListener("resize",a,{passive:!0}),()=>{window.removeEventListener("resize",a)})),[a]),{codeBlockRef:c,isEnabled:e,isCodeScrollable:n,toggle:r}}(),g=function(e){return e?.match(m)?.groups.title??""}(s)||r,{lineClassNames:x,code:y}=k(t,{metastring:s,language:b,magicComments:p}),C=i??function(e){return Boolean(e?.includes("showLineNumbers"))}(s);return(0,B.jsxs)(j,{as:"div",className:(0,c.Z)(n,b&&!n.includes(`language-${b}`)&&`language-${b}`),children:[g&&(0,B.jsx)("div",{className:v.codeBlockTitle,children:g}),(0,B.jsxs)("div",{className:v.codeBlockContent,children:[(0,B.jsx)(L.y$,{theme:f,code:y,language:b??"text",children:e=>{let{className:t,style:n,tokens:o,getLineProps:s,getTokenProps:r}=e;return(0,B.jsx)("pre",{tabIndex:0,ref:h.codeBlockRef,className:(0,c.Z)(t,v.codeBlock,"thin-scrollbar"),style:n,children:(0,B.jsx)("code",{className:(0,c.Z)(v.codeBlockLines,C&&v.codeBlockLinesWithNumbering),children:o.map(((e,t)=>(0,B.jsx)(I,{line:e,getLineProps:s,getTokenProps:r,classNames:x[t],showLineNumbers:C},t)))})})}}),(0,B.jsxs)("div",{className:v.buttonGroup,children:[(h.isEnabled||h.isCodeScrollable)&&(0,B.jsx)(Z,{className:v.codeButton,onClick:()=>h.toggle(),isEnabled:h.isEnabled}),(0,B.jsx)($,{className:v.codeButton,code:y})]})]})]})}function V(e){let{children:t,...n}=e;const c=(0,s.Z)(),r=function(e){return o.Children.toArray(e).some((e=>(0,o.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),a="string"==typeof r?H:y;return(0,B.jsx)(a,{...n,children:r},String(c))}},87594:(e,t)=>{function n(e){let t,n=[];for(let o of e.split(",").map((e=>e.trim())))if(/^-?\d+$/.test(o))n.push(parseInt(o,10));else if(t=o.match(/^(-?\d+)(-|\.\.\.?|\u2025|\u2026|\u22EF)(-?\d+)$/)){let[e,o,s,c]=t;if(o&&c){o=parseInt(o),c=parseInt(c);const e=o<c?1:-1;"-"!==s&&".."!==s&&"\u2025"!==s||(c+=e);for(let t=o;t!==c;t+=e)n.push(t)}}return n}t.default=n,e.exports=n},11151:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a,a:()=>r});var o=n(67294);const s={},c=o.createContext(s);function r(e){const t=o.useContext(c);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),o.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1772.6d5a31b9.js b/assets/js/1772.6d5a31b9.js new file mode 100644 index 000000000..a187bed55 --- /dev/null +++ b/assets/js/1772.6d5a31b9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1772],{5658:(e,t,n)=>{n.d(t,{Z:()=>a});n(67294);var i=n(36905),o=n(95999),s=n(92503),r=n(85893);function a(e){let{className:t}=e;return(0,r.jsx)("main",{className:(0,i.Z)("container margin-vert--xl",t),children:(0,r.jsx)("div",{className:"row",children:(0,r.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,r.jsx)(s.Z,{as:"h1",className:"hero__title",children:(0,r.jsx)(o.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,r.jsx)("p",{children:(0,r.jsx)(o.Z,{id:"theme.NotFound.p1",description:"The first paragraph of the 404 page",children:"We could not find what you were looking for."})}),(0,r.jsx)("p",{children:(0,r.jsx)(o.Z,{id:"theme.NotFound.p2",description:"The 2nd paragraph of the 404 page",children:"Please contact the owner of the site that linked you to the original URL and let them know their link is broken."})})]})})})}},51772:(e,t,n)=>{n.r(t),n.d(t,{default:()=>l});n(67294);var i=n(95999),o=n(71667),s=n(7372),r=n(5658),a=n(85893);function l(){const e=(0,i.I)({id:"theme.NotFound.title",message:"Page Not Found"});return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(o.d,{title:e}),(0,a.jsx)(s.Z,{children:(0,a.jsx)(r.Z,{})})]})}}}]); \ No newline at end of file diff --git a/assets/js/17896441.2a0b9e89.js b/assets/js/17896441.2a0b9e89.js new file mode 100644 index 000000000..1e1d01403 --- /dev/null +++ b/assets/js/17896441.2a0b9e89.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7918],{27362:(e,t,n)=>{n.r(t),n.d(t,{default:()=>ae});var s=n(67294),a=n(71667),i=n(902),l=n(85893);const o=s.createContext(null);function r(e){let{children:t,content:n}=e;const a=function(e){return(0,s.useMemo)((()=>({metadata:e.metadata,frontMatter:e.frontMatter,assets:e.assets,contentTitle:e.contentTitle,toc:e.toc})),[e])}(n);return(0,l.jsx)(o.Provider,{value:a,children:t})}function c(){const e=(0,s.useContext)(o);if(null===e)throw new i.i6("DocProvider");return e}function d(){const{metadata:e,frontMatter:t,assets:n}=c();return(0,l.jsx)(a.d,{title:e.title,description:e.description,keywords:t.keywords,image:n.image??t.image})}var u=n(36905),m=n(87524),h=n(95999),b=n(32244);function x(e){const{previous:t,next:n}=e;return(0,l.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,h.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"}),children:[t&&(0,l.jsx)(b.Z,{...t,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc",children:"Previous"})}),n&&(0,l.jsx)(b.Z,{...n,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc",children:"Next"}),isNext:!0})]})}function v(){const{metadata:e}=c();return(0,l.jsx)(x,{previous:e.previous,next:e.next})}var p=n(52263),g=n(33692),j=n(80143),f=n(35281),_=n(60373),N=n(74477);const C={unreleased:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unreleasedVersionLabel",description:"The label used to tell the user that he's browsing an unreleased doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is unreleased documentation for {siteTitle} {versionLabel} version."})},unmaintained:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unmaintainedVersionLabel",description:"The label used to tell the user that he's browsing an unmaintained doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained."})}};function Z(e){const t=C[e.versionMetadata.banner];return(0,l.jsx)(t,{...e})}function k(e){let{versionLabel:t,to:n,onClick:s}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionSuggestionLabel",description:"The label used to tell the user to check the latest version",values:{versionLabel:t,latestVersionLink:(0,l.jsx)("b",{children:(0,l.jsx)(g.Z,{to:n,onClick:s,children:(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionLinkLabel",description:"The label used for the latest version suggestion link label",children:"latest version"})})})},children:"For up-to-date documentation, see the {latestVersionLink} ({versionLabel})."})}function L(e){let{className:t,versionMetadata:n}=e;const{siteConfig:{title:s}}=(0,p.Z)(),{pluginId:a}=(0,j.gA)({failfast:!0}),{savePreferredVersionName:i}=(0,_.J)(a),{latestDocSuggestion:o,latestVersionSuggestion:r}=(0,j.Jo)(a),c=o??(d=r).docs.find((e=>e.id===d.mainDocId));var d;return(0,l.jsxs)("div",{className:(0,u.Z)(t,f.k.docs.docVersionBanner,"alert alert--warning margin-bottom--md"),role:"alert",children:[(0,l.jsx)("div",{children:(0,l.jsx)(Z,{siteTitle:s,versionMetadata:n})}),(0,l.jsx)("div",{className:"margin-top--md",children:(0,l.jsx)(k,{versionLabel:r.label,to:c.path,onClick:()=>i(r.name)})})]})}function T(e){let{className:t}=e;const n=(0,N.E)();return n.banner?(0,l.jsx)(L,{className:t,versionMetadata:n}):null}function M(e){let{className:t}=e;const n=(0,N.E)();return n.badge?(0,l.jsx)("span",{className:(0,u.Z)(t,f.k.docs.docVersionBadge,"badge badge--secondary"),children:(0,l.jsx)(h.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:n.label},children:"Version: {versionLabel}"})}):null}var I=n(71526),w=n(47265);function B(){const{metadata:e}=c(),{editUrl:t,lastUpdatedAt:n,lastUpdatedBy:s,tags:a}=e,i=a.length>0,o=!!(t||n||s);return i||o?(0,l.jsxs)("footer",{className:(0,u.Z)(f.k.docs.docFooter,"docusaurus-mt-lg"),children:[i&&(0,l.jsx)("div",{className:(0,u.Z)("row margin-top--sm",f.k.docs.docFooterTagsRow),children:(0,l.jsx)("div",{className:"col",children:(0,l.jsx)(I.Z,{tags:a})})}),o&&(0,l.jsx)(w.Z,{className:(0,u.Z)("margin-top--sm",f.k.docs.docFooterEditMetaRow),editUrl:t,lastUpdatedAt:n,lastUpdatedBy:s})]}):null}var V=n(86043),H=n(93743);const y={tocCollapsibleButton:"tocCollapsibleButton_TO0P",tocCollapsibleButtonExpanded:"tocCollapsibleButtonExpanded_MG3E"};function E(e){let{collapsed:t,...n}=e;return(0,l.jsx)("button",{type:"button",...n,className:(0,u.Z)("clean-btn",y.tocCollapsibleButton,!t&&y.tocCollapsibleButtonExpanded,n.className),children:(0,l.jsx)(h.Z,{id:"theme.TOCCollapsible.toggleButtonLabel",description:"The label used by the button on the collapsible TOC component",children:"On this page"})})}const A={tocCollapsible:"tocCollapsible_ETCw",tocCollapsibleContent:"tocCollapsibleContent_vkbj",tocCollapsibleExpanded:"tocCollapsibleExpanded_sAul"};function P(e){let{toc:t,className:n,minHeadingLevel:s,maxHeadingLevel:a}=e;const{collapsed:i,toggleCollapsed:o}=(0,V.u)({initialState:!0});return(0,l.jsxs)("div",{className:(0,u.Z)(A.tocCollapsible,!i&&A.tocCollapsibleExpanded,n),children:[(0,l.jsx)(E,{collapsed:i,onClick:o}),(0,l.jsx)(V.z,{lazy:!0,className:A.tocCollapsibleContent,collapsed:i,children:(0,l.jsx)(H.Z,{toc:t,minHeadingLevel:s,maxHeadingLevel:a})})]})}const F={tocMobile:"tocMobile_ITEo"};function R(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(P,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:(0,u.Z)(f.k.docs.docTocMobile,F.tocMobile)})}var S=n(39407);function U(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(S.Z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:f.k.docs.docTocDesktop})}var D=n(92503),O=n(95896);function z(e){let{children:t}=e;const n=function(){const{metadata:e,frontMatter:t,contentTitle:n}=c();return t.hide_title||void 0!==n?null:e.title}();return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docMarkdown,"markdown"),children:[n&&(0,l.jsx)("header",{children:(0,l.jsx)(D.Z,{as:"h1",children:n})}),(0,l.jsx)(O.Z,{children:t})]})}var G=n(53438),W=n(48596),J=n(44996);function Q(e){return(0,l.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,l.jsx)("path",{d:"M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z",fill:"currentColor"})})}const X={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function Y(){const e=(0,J.Z)("/");return(0,l.jsx)("li",{className:"breadcrumbs__item",children:(0,l.jsx)(g.Z,{"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.home",message:"Home page",description:"The ARIA label for the home page in the breadcrumbs"}),className:"breadcrumbs__link",href:e,children:(0,l.jsx)(Q,{className:X.breadcrumbHomeIcon})})})}const $={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function q(e){let{children:t,href:n,isLast:s}=e;const a="breadcrumbs__link";return s?(0,l.jsx)("span",{className:a,itemProp:"name",children:t}):n?(0,l.jsx)(g.Z,{className:a,href:n,itemProp:"item",children:(0,l.jsx)("span",{itemProp:"name",children:t})}):(0,l.jsx)("span",{className:a,children:t})}function K(e){let{children:t,active:n,index:s,addMicrodata:a}=e;return(0,l.jsxs)("li",{...a&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},className:(0,u.Z)("breadcrumbs__item",{"breadcrumbs__item--active":n}),children:[t,(0,l.jsx)("meta",{itemProp:"position",content:String(s+1)})]})}function ee(){const e=(0,G.s1)(),t=(0,W.Ns)();return e?(0,l.jsx)("nav",{className:(0,u.Z)(f.k.docs.docBreadcrumbs,$.breadcrumbsContainer),"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.navAriaLabel",message:"Breadcrumbs",description:"The ARIA label for the breadcrumbs"}),children:(0,l.jsxs)("ul",{className:"breadcrumbs",itemScope:!0,itemType:"https://schema.org/BreadcrumbList",children:[t&&(0,l.jsx)(Y,{}),e.map(((t,n)=>{const s=n===e.length-1,a="category"===t.type&&t.linkUnlisted?void 0:t.href;return(0,l.jsx)(K,{active:s,index:n,addMicrodata:!!a,children:(0,l.jsx)(q,{href:a,isLast:s,children:t.label})},n)}))]})}):null}var te=n(22212);const ne={docItemContainer:"docItemContainer_Djhp",docItemCol:"docItemCol_VOVn"};function se(e){let{children:t}=e;const n=function(){const{frontMatter:e,toc:t}=c(),n=(0,m.i)(),s=e.hide_table_of_contents,a=!s&&t.length>0;return{hidden:s,mobile:a?(0,l.jsx)(R,{}):void 0,desktop:!a||"desktop"!==n&&"ssr"!==n?void 0:(0,l.jsx)(U,{})}}(),{metadata:{unlisted:s}}=c();return(0,l.jsxs)("div",{className:"row",children:[(0,l.jsxs)("div",{className:(0,u.Z)("col",!n.hidden&&ne.docItemCol),children:[s&&(0,l.jsx)(te.Z,{}),(0,l.jsx)(T,{}),(0,l.jsxs)("div",{className:ne.docItemContainer,children:[(0,l.jsxs)("article",{children:[(0,l.jsx)(ee,{}),(0,l.jsx)(M,{}),n.mobile,(0,l.jsx)(z,{children:t}),(0,l.jsx)(B,{})]}),(0,l.jsx)(v,{})]})]}),n.desktop&&(0,l.jsx)("div",{className:"col col--3",children:n.desktop})]})}function ae(e){const t=`docs-doc-id-${e.content.metadata.id}`,n=e.content;return(0,l.jsx)(r,{content:e.content,children:(0,l.jsxs)(a.FG,{className:t,children:[(0,l.jsx)(d,{}),(0,l.jsx)(se,{children:(0,l.jsx)(n,{})})]})})}},32244:(e,t,n)=>{n.d(t,{Z:()=>l});n(67294);var s=n(36905),a=n(33692),i=n(85893);function l(e){const{permalink:t,title:n,subLabel:l,isNext:o}=e;return(0,i.jsxs)(a.Z,{className:(0,s.Z)("pagination-nav__link",o?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[l&&(0,i.jsx)("div",{className:"pagination-nav__sublabel",children:l}),(0,i.jsx)("div",{className:"pagination-nav__label",children:n})]})}},13008:(e,t,n)=>{n.d(t,{Z:()=>o});n(67294);var s=n(36905),a=n(33692);const i={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=n(85893);function o(e){let{permalink:t,label:n,count:o}=e;return(0,l.jsxs)(a.Z,{href:t,className:(0,s.Z)(i.tag,o?i.tagWithCount:i.tagRegular),children:[n,o&&(0,l.jsx)("span",{children:o})]})}},71526:(e,t,n)=>{n.d(t,{Z:()=>r});n(67294);var s=n(36905),a=n(95999),i=n(13008);const l={tags:"tags_jXut",tag:"tag_QGVx"};var o=n(85893);function r(e){let{tags:t}=e;return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("b",{children:(0,o.jsx)(a.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,o.jsx)("ul",{className:(0,s.Z)(l.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:n}=e;return(0,o.jsx)("li",{className:l.tag,children:(0,o.jsx)(i.Z,{label:t,permalink:n})},n)}))})]})}}}]); \ No newline at end of file diff --git a/assets/js/17896441.f5808a1a.js b/assets/js/17896441.f5808a1a.js deleted file mode 100644 index 819497098..000000000 --- a/assets/js/17896441.f5808a1a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7918],{61694:(e,t,n)=>{n.r(t),n.d(t,{default:()=>ae});var s=n(67294),a=n(62581),i=n(93478),l=n(85893);const o=s.createContext(null);function r(e){let{children:t,content:n}=e;const a=function(e){return(0,s.useMemo)((()=>({metadata:e.metadata,frontMatter:e.frontMatter,assets:e.assets,contentTitle:e.contentTitle,toc:e.toc})),[e])}(n);return(0,l.jsx)(o.Provider,{value:a,children:t})}function c(){const e=(0,s.useContext)(o);if(null===e)throw new i.i6("DocProvider");return e}function d(){const{metadata:e,frontMatter:t,assets:n}=c();return(0,l.jsx)(a.d,{title:e.title,description:e.description,keywords:t.keywords,image:n.image??t.image})}var u=n(36905),m=n(94980),h=n(11614),b=n(16948);function x(e){const{previous:t,next:n}=e;return(0,l.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,h.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"}),children:[t&&(0,l.jsx)(b.Z,{...t,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc",children:"Previous"})}),n&&(0,l.jsx)(b.Z,{...n,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc",children:"Next"}),isNext:!0})]})}function v(){const{metadata:e}=c();return(0,l.jsx)(x,{previous:e.previous,next:e.next})}var p=n(6832),g=n(75013),j=n(4452),f=n(18015),_=n(4049),N=n(6141);const C={unreleased:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unreleasedVersionLabel",description:"The label used to tell the user that he's browsing an unreleased doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is unreleased documentation for {siteTitle} {versionLabel} version."})},unmaintained:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unmaintainedVersionLabel",description:"The label used to tell the user that he's browsing an unmaintained doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained."})}};function Z(e){const t=C[e.versionMetadata.banner];return(0,l.jsx)(t,{...e})}function k(e){let{versionLabel:t,to:n,onClick:s}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionSuggestionLabel",description:"The label used to tell the user to check the latest version",values:{versionLabel:t,latestVersionLink:(0,l.jsx)("b",{children:(0,l.jsx)(g.Z,{to:n,onClick:s,children:(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionLinkLabel",description:"The label used for the latest version suggestion link label",children:"latest version"})})})},children:"For up-to-date documentation, see the {latestVersionLink} ({versionLabel})."})}function L(e){let{className:t,versionMetadata:n}=e;const{siteConfig:{title:s}}=(0,p.Z)(),{pluginId:a}=(0,j.gA)({failfast:!0}),{savePreferredVersionName:i}=(0,_.J)(a),{latestDocSuggestion:o,latestVersionSuggestion:r}=(0,j.Jo)(a),c=o??(d=r).docs.find((e=>e.id===d.mainDocId));var d;return(0,l.jsxs)("div",{className:(0,u.Z)(t,f.k.docs.docVersionBanner,"alert alert--warning margin-bottom--md"),role:"alert",children:[(0,l.jsx)("div",{children:(0,l.jsx)(Z,{siteTitle:s,versionMetadata:n})}),(0,l.jsx)("div",{className:"margin-top--md",children:(0,l.jsx)(k,{versionLabel:r.label,to:c.path,onClick:()=>i(r.name)})})]})}function T(e){let{className:t}=e;const n=(0,N.E)();return n.banner?(0,l.jsx)(L,{className:t,versionMetadata:n}):null}function M(e){let{className:t}=e;const n=(0,N.E)();return n.badge?(0,l.jsx)("span",{className:(0,u.Z)(t,f.k.docs.docVersionBadge,"badge badge--secondary"),children:(0,l.jsx)(h.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:n.label},children:"Version: {versionLabel}"})}):null}var I=n(58045),w=n(18842);function B(){const{metadata:e}=c(),{editUrl:t,lastUpdatedAt:n,lastUpdatedBy:s,tags:a}=e,i=a.length>0,o=!!(t||n||s);return i||o?(0,l.jsxs)("footer",{className:(0,u.Z)(f.k.docs.docFooter,"docusaurus-mt-lg"),children:[i&&(0,l.jsx)("div",{className:(0,u.Z)("row margin-top--sm",f.k.docs.docFooterTagsRow),children:(0,l.jsx)("div",{className:"col",children:(0,l.jsx)(I.Z,{tags:a})})}),o&&(0,l.jsx)(w.Z,{className:(0,u.Z)("margin-top--sm",f.k.docs.docFooterEditMetaRow),editUrl:t,lastUpdatedAt:n,lastUpdatedBy:s})]}):null}var V=n(17940),H=n(21351);const y={tocCollapsibleButton:"tocCollapsibleButton_TO0P",tocCollapsibleButtonExpanded:"tocCollapsibleButtonExpanded_MG3E"};function E(e){let{collapsed:t,...n}=e;return(0,l.jsx)("button",{type:"button",...n,className:(0,u.Z)("clean-btn",y.tocCollapsibleButton,!t&&y.tocCollapsibleButtonExpanded,n.className),children:(0,l.jsx)(h.Z,{id:"theme.TOCCollapsible.toggleButtonLabel",description:"The label used by the button on the collapsible TOC component",children:"On this page"})})}const A={tocCollapsible:"tocCollapsible_ETCw",tocCollapsibleContent:"tocCollapsibleContent_vkbj",tocCollapsibleExpanded:"tocCollapsibleExpanded_sAul"};function P(e){let{toc:t,className:n,minHeadingLevel:s,maxHeadingLevel:a}=e;const{collapsed:i,toggleCollapsed:o}=(0,V.u)({initialState:!0});return(0,l.jsxs)("div",{className:(0,u.Z)(A.tocCollapsible,!i&&A.tocCollapsibleExpanded,n),children:[(0,l.jsx)(E,{collapsed:i,onClick:o}),(0,l.jsx)(V.z,{lazy:!0,className:A.tocCollapsibleContent,collapsed:i,children:(0,l.jsx)(H.Z,{toc:t,minHeadingLevel:s,maxHeadingLevel:a})})]})}const F={tocMobile:"tocMobile_ITEo"};function R(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(P,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:(0,u.Z)(f.k.docs.docTocMobile,F.tocMobile)})}var S=n(95967);function U(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(S.Z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:f.k.docs.docTocDesktop})}var D=n(34055),O=n(33658);function z(e){let{children:t}=e;const n=function(){const{metadata:e,frontMatter:t,contentTitle:n}=c();return t.hide_title||void 0!==n?null:e.title}();return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docMarkdown,"markdown"),children:[n&&(0,l.jsx)("header",{children:(0,l.jsx)(D.Z,{as:"h1",children:n})}),(0,l.jsx)(O.Z,{children:t})]})}var G=n(85919),W=n(18407),J=n(51402);function Q(e){return(0,l.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,l.jsx)("path",{d:"M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z",fill:"currentColor"})})}const X={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function Y(){const e=(0,J.Z)("/");return(0,l.jsx)("li",{className:"breadcrumbs__item",children:(0,l.jsx)(g.Z,{"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.home",message:"Home page",description:"The ARIA label for the home page in the breadcrumbs"}),className:"breadcrumbs__link",href:e,children:(0,l.jsx)(Q,{className:X.breadcrumbHomeIcon})})})}const $={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function q(e){let{children:t,href:n,isLast:s}=e;const a="breadcrumbs__link";return s?(0,l.jsx)("span",{className:a,itemProp:"name",children:t}):n?(0,l.jsx)(g.Z,{className:a,href:n,itemProp:"item",children:(0,l.jsx)("span",{itemProp:"name",children:t})}):(0,l.jsx)("span",{className:a,children:t})}function K(e){let{children:t,active:n,index:s,addMicrodata:a}=e;return(0,l.jsxs)("li",{...a&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},className:(0,u.Z)("breadcrumbs__item",{"breadcrumbs__item--active":n}),children:[t,(0,l.jsx)("meta",{itemProp:"position",content:String(s+1)})]})}function ee(){const e=(0,G.s1)(),t=(0,W.Ns)();return e?(0,l.jsx)("nav",{className:(0,u.Z)(f.k.docs.docBreadcrumbs,$.breadcrumbsContainer),"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.navAriaLabel",message:"Breadcrumbs",description:"The ARIA label for the breadcrumbs"}),children:(0,l.jsxs)("ul",{className:"breadcrumbs",itemScope:!0,itemType:"https://schema.org/BreadcrumbList",children:[t&&(0,l.jsx)(Y,{}),e.map(((t,n)=>{const s=n===e.length-1,a="category"===t.type&&t.linkUnlisted?void 0:t.href;return(0,l.jsx)(K,{active:s,index:n,addMicrodata:!!a,children:(0,l.jsx)(q,{href:a,isLast:s,children:t.label})},n)}))]})}):null}var te=n(94007);const ne={docItemContainer:"docItemContainer_Djhp",docItemCol:"docItemCol_VOVn"};function se(e){let{children:t}=e;const n=function(){const{frontMatter:e,toc:t}=c(),n=(0,m.i)(),s=e.hide_table_of_contents,a=!s&&t.length>0;return{hidden:s,mobile:a?(0,l.jsx)(R,{}):void 0,desktop:!a||"desktop"!==n&&"ssr"!==n?void 0:(0,l.jsx)(U,{})}}(),{metadata:{unlisted:s}}=c();return(0,l.jsxs)("div",{className:"row",children:[(0,l.jsxs)("div",{className:(0,u.Z)("col",!n.hidden&&ne.docItemCol),children:[s&&(0,l.jsx)(te.Z,{}),(0,l.jsx)(T,{}),(0,l.jsxs)("div",{className:ne.docItemContainer,children:[(0,l.jsxs)("article",{children:[(0,l.jsx)(ee,{}),(0,l.jsx)(M,{}),n.mobile,(0,l.jsx)(z,{children:t}),(0,l.jsx)(B,{})]}),(0,l.jsx)(v,{})]})]}),n.desktop&&(0,l.jsx)("div",{className:"col col--3",children:n.desktop})]})}function ae(e){const t=`docs-doc-id-${e.content.metadata.id}`,n=e.content;return(0,l.jsx)(r,{content:e.content,children:(0,l.jsxs)(a.FG,{className:t,children:[(0,l.jsx)(d,{}),(0,l.jsx)(se,{children:(0,l.jsx)(n,{})})]})})}},16948:(e,t,n)=>{n.d(t,{Z:()=>l});n(67294);var s=n(36905),a=n(75013),i=n(85893);function l(e){const{permalink:t,title:n,subLabel:l,isNext:o}=e;return(0,i.jsxs)(a.Z,{className:(0,s.Z)("pagination-nav__link",o?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[l&&(0,i.jsx)("div",{className:"pagination-nav__sublabel",children:l}),(0,i.jsx)("div",{className:"pagination-nav__label",children:n})]})}},24588:(e,t,n)=>{n.d(t,{Z:()=>o});n(67294);var s=n(36905),a=n(75013);const i={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=n(85893);function o(e){let{permalink:t,label:n,count:o}=e;return(0,l.jsxs)(a.Z,{href:t,className:(0,s.Z)(i.tag,o?i.tagWithCount:i.tagRegular),children:[n,o&&(0,l.jsx)("span",{children:o})]})}},58045:(e,t,n)=>{n.d(t,{Z:()=>r});n(67294);var s=n(36905),a=n(11614),i=n(24588);const l={tags:"tags_jXut",tag:"tag_QGVx"};var o=n(85893);function r(e){let{tags:t}=e;return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("b",{children:(0,o.jsx)(a.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,o.jsx)("ul",{className:(0,s.Z)(l.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:n}=e;return(0,o.jsx)("li",{className:l.tag,children:(0,o.jsx)(i.Z,{label:t,permalink:n})},n)}))})]})}}}]); \ No newline at end of file diff --git a/assets/js/18793598.dc1cbbe0.js b/assets/js/18793598.d6ba6946.js similarity index 98% rename from assets/js/18793598.dc1cbbe0.js rename to assets/js/18793598.d6ba6946.js index 60dddd44a..544721942 100644 --- a/assets/js/18793598.dc1cbbe0.js +++ b/assets/js/18793598.d6ba6946.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2278],{83951:(t,i,s)=>{s.r(i),s.d(i,{default:()=>f});var e=s(67294),h=s(85893);function n(t,i){return Math.floor(Math.random()*(i-t+1)+t)}function o(t,i,s,e){const h=e*Math.PI/180;return[t+s*Math.cos(h),i+s*Math.sin(h)]}function r(t,i,s,e){return 180*Math.atan2(e-i,s-t)/Math.PI}function a(t,i,s,e,h,n,o,r,a,c,u,d){this.ctx=t,this.init(i,s,e,h,n,o,r,a,c,u,d)}function c(t,i,s,e,h,n){this.ctx=t,this.init(i,s,e,h,n)}function u(t,i,s,e,h,n){t.beginPath(),t.moveTo(i,s),t.lineTo(e,h),t.lineWidth=n,t.strokeStyle="#F6CFC7",t.stroke();const o=t.createLinearGradient(i,s,e,h);o.addColorStop(0,"#F60809"),o.addColorStop(1,"#F6B9BD"),t.strokeStyle=o,t.lineWidth=n,t.shadowBlur=10,t.shadowColor="red",t.stroke()}function d(t,i,e,h){const o=t.getContext("2d"),r=i/2,d=e/2;let l,f,x;h?(l="#8d3838",f="#6e2b2b",x="#6e2b2b"):(l="#ffd4d4",f="#e6e8eb",x="#ffd4d4");const p=[],w=[],y=e/7,M=y/15,m=s.g.requestAnimationFrame||s.g.mozRequestAnimationFrame||s.g.webkitRequestAnimationFrame||s.g.msRequestAnimationFrame||function(t){setTimeout(t,17)};for(let s=0;s<3;s+=1){const t=new c(o,i,e,n(0,i),n(0,e),l);p.push(t)}w.push(new a(o,i,e,r,d,y,2.65*y,9*M,0,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,90,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,180,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,270,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,45,1.5,2,x)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,135,1.5,2,x)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,225,1.5,2,x));let g=0;const P="up"==localStorage.getItem("lights");m((function s(n){if(i<=1)return;const r=(n-g)/1e3;if(null!==t.offsetParent){o.clearRect(0,0,i,e);for(let t=0;t<p.length;t+=1)p[t].render(r);for(let t=0;t<w.length;t+=1)w[t].render(r);h&&P&&i>1280?(Math.random()>.95&&function(t,i,s){const e=i/2+30*(.5-Math.random()),h=s/2+30*(.5-Math.random()),n=Math.floor(10*Math.random())+2;let o=e,r=h;const a=Math.random()*Math.PI*2;t.globalCompositeOperation="lighter";for(let c=0;c<n;c++){const i=10*Math.random(),s=a+(Math.random()-.5)*Math.PI/3,e=o+Math.cos(s)*i,h=r+Math.sin(s)*i;u(t,o,r,e,h,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s+Math.PI/4)*i,r+Math.sin(s+Math.PI/4)*i,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s-Math.PI/4)*i,r+Math.sin(s-Math.PI/4)*i,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s-Math.PI/4)*i,r+Math.sin(s-Math.PI/4)*i,3),o=e,r=h}t.globalCompositeOperation="source-over"}(o,i,e),o.shadowBlur=100):o.shadowBlur=0}g=n,m(s)}))}let l;if(a.prototype.init=function(t,i,s,e,h,n,o,r,a,c,u){this.X=t,this.Y=i,this.radius=h,this.x=s,this.y=e,this.r=n,this.w=o,this.c=u,this.rotate=r,this.speed=60*a,this.angleDiff=c,this.a=0},a.prototype.drawSegment=function(t,i,s){this.ctx.translate(this.x,this.y),this.ctx.rotate(s*Math.PI/180),this.ctx.translate(-this.x,-this.y),this.ctx.beginPath();const e=o(this.x,this.y,this.r,t),h=e[0],n=e[1],a=o(this.x,this.y,this.r,i),c=a[0],u=a[1],d=h-this.w,l=u-this.w,f=r(this.x,this.y,d,n),x=r(this.x,this.y,c,l),p=i*Math.PI/180,w=t*Math.PI/180,y=f*Math.PI/180,M=x*Math.PI/180;this.ctx.arc(this.x,this.y,this.r,p,w,!0),this.ctx.arc(this.x,this.y,this.r-this.w,y,M,!1),this.ctx.closePath(),this.ctx.fillStyle=this.c,this.ctx.fill(),this.ctx.stroke()},a.prototype.draw=function(){this.ctx.save(),this.ctx.lineWidth=3,this.ctx.strokeStyle=this.c,this.ctx.shadowColor=this.c,this.drawSegment(4+this.angleDiff,86-this.angleDiff,this.rotate+this.a),this.ctx.restore()},a.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},a.prototype.updateParams=function(t){this.a+=this.speed*t*this.radius/this.r},a.prototype.render=function(t){this.updateParams(t),this.draw()},c.prototype.init=function(t,i,s,e,h){this.X=t,this.Y=i,this.x=s,this.y=e,this.c=h,this.lw=1,this.v={x:100*Math.random(),y:100*Math.random()}},c.prototype.draw=function(){this.ctx.save(),this.ctx.lineWidth=this.lw,this.ctx.strokeStyle=this.c,this.ctx.beginPath(),this.ctx.moveTo(0,this.y),this.ctx.lineTo(this.X,this.y),this.ctx.stroke(),this.ctx.lineWidth=this.lw,this.ctx.beginPath(),this.ctx.moveTo(this.x,0),this.ctx.lineTo(this.x,this.Y),this.ctx.stroke(),this.ctx.restore()},c.prototype.updatePosition=function(t){this.x+=this.v.x*t,this.y+=this.v.y*t},c.prototype.wrapPosition=function(){this.x<0&&(this.x=this.X),this.x>this.X&&(this.x=0),this.y<0&&(this.y=this.Y),this.y>this.Y&&(this.y=0)},c.prototype.render=function(t){this.updatePosition(t),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){l=new MutationObserver((function(t){t.forEach((function(t){"attributes"==t.type&&window.dispatchEvent(new Event("resized"))}))}));const t=document.querySelector("html");l.observe(t,{attributes:!0})}const f=t=>{const[i,n]=e.useState({x:1,y:1}),o=e.useRef(null),r=()=>{null!==o.current&&(o.current.width=o.current.clientWidth,o.current.height=o.current.clientHeight,n({x:o.current?o.current.clientWidth:0,y:o.current?o.current.clientHeight:0}))};return e.useEffect((()=>r()),[]),(s.g.window||process&&process.browser)&&(e.useEffect((()=>(window.addEventListener("resize",r),()=>window.removeEventListener("resize",r)))),e.useEffect((()=>(window.addEventListener("resized",r),()=>window.removeEventListener("resized",r))))),e.useEffect((()=>{d(o.current,i.x,i.y,t.isDarkTheme)}),[i]),(0,h.jsx)("canvas",{ref:o,style:{width:"100%",height:"100%"}})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2278],{53225:(t,i,s)=>{s.r(i),s.d(i,{default:()=>f});var e=s(67294),h=s(85893);function n(t,i){return Math.floor(Math.random()*(i-t+1)+t)}function o(t,i,s,e){const h=e*Math.PI/180;return[t+s*Math.cos(h),i+s*Math.sin(h)]}function r(t,i,s,e){return 180*Math.atan2(e-i,s-t)/Math.PI}function a(t,i,s,e,h,n,o,r,a,c,u,d){this.ctx=t,this.init(i,s,e,h,n,o,r,a,c,u,d)}function c(t,i,s,e,h,n){this.ctx=t,this.init(i,s,e,h,n)}function u(t,i,s,e,h,n){t.beginPath(),t.moveTo(i,s),t.lineTo(e,h),t.lineWidth=n,t.strokeStyle="#F6CFC7",t.stroke();const o=t.createLinearGradient(i,s,e,h);o.addColorStop(0,"#F60809"),o.addColorStop(1,"#F6B9BD"),t.strokeStyle=o,t.lineWidth=n,t.shadowBlur=10,t.shadowColor="red",t.stroke()}function d(t,i,e,h){const o=t.getContext("2d"),r=i/2,d=e/2;let l,f,x;h?(l="#8d3838",f="#6e2b2b",x="#6e2b2b"):(l="#ffd4d4",f="#e6e8eb",x="#ffd4d4");const p=[],w=[],y=e/7,M=y/15,m=s.g.requestAnimationFrame||s.g.mozRequestAnimationFrame||s.g.webkitRequestAnimationFrame||s.g.msRequestAnimationFrame||function(t){setTimeout(t,17)};for(let s=0;s<3;s+=1){const t=new c(o,i,e,n(0,i),n(0,e),l);p.push(t)}w.push(new a(o,i,e,r,d,y,2.65*y,9*M,0,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,90,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,180,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,2.65*y,9*M,270,-1.5,0,f)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,45,1.5,2,x)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,135,1.5,2,x)),w.push(new a(o,i,e,r,d,y,1.45*y,8*M,225,1.5,2,x));let g=0;const P="up"==localStorage.getItem("lights");m((function s(n){if(i<=1)return;const r=(n-g)/1e3;if(null!==t.offsetParent){o.clearRect(0,0,i,e);for(let t=0;t<p.length;t+=1)p[t].render(r);for(let t=0;t<w.length;t+=1)w[t].render(r);h&&P&&i>1280?(Math.random()>.95&&function(t,i,s){const e=i/2+30*(.5-Math.random()),h=s/2+30*(.5-Math.random()),n=Math.floor(10*Math.random())+2;let o=e,r=h;const a=Math.random()*Math.PI*2;t.globalCompositeOperation="lighter";for(let c=0;c<n;c++){const i=10*Math.random(),s=a+(Math.random()-.5)*Math.PI/3,e=o+Math.cos(s)*i,h=r+Math.sin(s)*i;u(t,o,r,e,h,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s+Math.PI/4)*i,r+Math.sin(s+Math.PI/4)*i,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s-Math.PI/4)*i,r+Math.sin(s-Math.PI/4)*i,3),Math.random()>.7&&u(t,o,r,o+Math.cos(s-Math.PI/4)*i,r+Math.sin(s-Math.PI/4)*i,3),o=e,r=h}t.globalCompositeOperation="source-over"}(o,i,e),o.shadowBlur=100):o.shadowBlur=0}g=n,m(s)}))}let l;if(a.prototype.init=function(t,i,s,e,h,n,o,r,a,c,u){this.X=t,this.Y=i,this.radius=h,this.x=s,this.y=e,this.r=n,this.w=o,this.c=u,this.rotate=r,this.speed=60*a,this.angleDiff=c,this.a=0},a.prototype.drawSegment=function(t,i,s){this.ctx.translate(this.x,this.y),this.ctx.rotate(s*Math.PI/180),this.ctx.translate(-this.x,-this.y),this.ctx.beginPath();const e=o(this.x,this.y,this.r,t),h=e[0],n=e[1],a=o(this.x,this.y,this.r,i),c=a[0],u=a[1],d=h-this.w,l=u-this.w,f=r(this.x,this.y,d,n),x=r(this.x,this.y,c,l),p=i*Math.PI/180,w=t*Math.PI/180,y=f*Math.PI/180,M=x*Math.PI/180;this.ctx.arc(this.x,this.y,this.r,p,w,!0),this.ctx.arc(this.x,this.y,this.r-this.w,y,M,!1),this.ctx.closePath(),this.ctx.fillStyle=this.c,this.ctx.fill(),this.ctx.stroke()},a.prototype.draw=function(){this.ctx.save(),this.ctx.lineWidth=3,this.ctx.strokeStyle=this.c,this.ctx.shadowColor=this.c,this.drawSegment(4+this.angleDiff,86-this.angleDiff,this.rotate+this.a),this.ctx.restore()},a.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},a.prototype.updateParams=function(t){this.a+=this.speed*t*this.radius/this.r},a.prototype.render=function(t){this.updateParams(t),this.draw()},c.prototype.init=function(t,i,s,e,h){this.X=t,this.Y=i,this.x=s,this.y=e,this.c=h,this.lw=1,this.v={x:100*Math.random(),y:100*Math.random()}},c.prototype.draw=function(){this.ctx.save(),this.ctx.lineWidth=this.lw,this.ctx.strokeStyle=this.c,this.ctx.beginPath(),this.ctx.moveTo(0,this.y),this.ctx.lineTo(this.X,this.y),this.ctx.stroke(),this.ctx.lineWidth=this.lw,this.ctx.beginPath(),this.ctx.moveTo(this.x,0),this.ctx.lineTo(this.x,this.Y),this.ctx.stroke(),this.ctx.restore()},c.prototype.updatePosition=function(t){this.x+=this.v.x*t,this.y+=this.v.y*t},c.prototype.wrapPosition=function(){this.x<0&&(this.x=this.X),this.x>this.X&&(this.x=0),this.y<0&&(this.y=this.Y),this.y>this.Y&&(this.y=0)},c.prototype.render=function(t){this.updatePosition(t),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){l=new MutationObserver((function(t){t.forEach((function(t){"attributes"==t.type&&window.dispatchEvent(new Event("resized"))}))}));const t=document.querySelector("html");l.observe(t,{attributes:!0})}const f=t=>{const[i,n]=e.useState({x:1,y:1}),o=e.useRef(null),r=()=>{null!==o.current&&(o.current.width=o.current.clientWidth,o.current.height=o.current.clientHeight,n({x:o.current?o.current.clientWidth:0,y:o.current?o.current.clientHeight:0}))};return e.useEffect((()=>r()),[]),(s.g.window||process&&process.browser)&&(e.useEffect((()=>(window.addEventListener("resize",r),()=>window.removeEventListener("resize",r)))),e.useEffect((()=>(window.addEventListener("resized",r),()=>window.removeEventListener("resized",r))))),e.useEffect((()=>{d(o.current,i.x,i.y,t.isDarkTheme)}),[i]),(0,h.jsx)("canvas",{ref:o,style:{width:"100%",height:"100%"}})}}}]); \ No newline at end of file diff --git a/assets/js/192a8b1e.68037fd1.js b/assets/js/192a8b1e.55937a32.js similarity index 99% rename from assets/js/192a8b1e.68037fd1.js rename to assets/js/192a8b1e.55937a32.js index 75407b1d6..d6564f640 100644 --- a/assets/js/192a8b1e.68037fd1.js +++ b/assets/js/192a8b1e.55937a32.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5069],{47262:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var i=s(85893),t=s(11151);const r={id:"engines",title:"Engines and scalability"},o=void 0,l={id:"server/engines",title:"Engines and scalability",description:"The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data.",source:"@site/docs/server/engines.md",sourceDirName:"server",slug:"/server/engines",permalink:"/docs/server/engines",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/engines.md",tags:[],version:"current",frontMatter:{id:"engines",title:"Engines and scalability"},sidebar:"Guides",previous:{title:"Server-side subscriptions",permalink:"/docs/server/server_subs"},next:{title:"Async consumers",permalink:"/docs/server/consumers"}},a={},d=[{value:"Memory engine",id:"memory-engine",level:2},{value:"Redis engine",id:"redis-engine",level:2},{value:"Redis engine options",id:"redis-engine-options",level:3},{value:"redis_address",id:"redis_address",level:4},{value:"redis_password",id:"redis_password",level:4},{value:"redis_user",id:"redis_user",level:4},{value:"redis_db",id:"redis_db",level:4},{value:"redis_prefix",id:"redis_prefix",level:4},{value:"redis_use_lists",id:"redis_use_lists",level:4},{value:"redis_force_resp2",id:"redis_force_resp2",level:4},{value:"Configuring Redis TLS",id:"configuring-redis-tls",level:3},{value:"redis_tls",id:"redis_tls",level:4},{value:"redis_tls_insecure_skip_verify",id:"redis_tls_insecure_skip_verify",level:4},{value:"redis_tls_cert",id:"redis_tls_cert",level:4},{value:"redis_tls_key",id:"redis_tls_key",level:4},{value:"redis_tls_root_ca",id:"redis_tls_root_ca",level:4},{value:"redis_tls_server_name",id:"redis_tls_server_name",level:4},{value:"Scaling with Redis tutorial",id:"scaling-with-redis-tutorial",level:3},{value:"Redis Sentinel for high availability",id:"redis-sentinel-for-high-availability",level:3},{value:"Redis Sentinel TLS",id:"redis-sentinel-tls",level:3},{value:"redis_sentinel_tls",id:"redis_sentinel_tls",level:4},{value:"redis_sentinel_tls_insecure_skip_verify",id:"redis_sentinel_tls_insecure_skip_verify",level:4},{value:"redis_sentinel_tls_cert",id:"redis_sentinel_tls_cert",level:4},{value:"redis_sentinel_tls_key",id:"redis_sentinel_tls_key",level:4},{value:"redis_sentinel_tls_root_ca",id:"redis_sentinel_tls_root_ca",level:4},{value:"redis_sentinel_tls_server_name",id:"redis_sentinel_tls_server_name",level:4},{value:"Haproxy instead of Sentinel configuration",id:"haproxy-instead-of-sentinel-configuration",level:3},{value:"Redis sharding",id:"redis-sharding",level:3},{value:"Redis cluster",id:"redis-cluster",level:3},{value:"Optimize getting presence stats",id:"optimize-getting-presence-stats",level:3},{value:"Other Redis compatible",id:"other-redis-compatible",level:2},{value:"Tarantool engine",id:"tarantool-engine",level:2},{value:"Tarantool engine options",id:"tarantool-engine-options",level:3},{value:"tarantool_address",id:"tarantool_address",level:4},{value:"tarantool_mode",id:"tarantool_mode",level:4},{value:"tarantool_user",id:"tarantool_user",level:4},{value:"tarantool_password",id:"tarantool_password",level:4},{value:"Tarantool engine limitations",id:"tarantool-engine-limitations",level:3},{value:"Nats broker",id:"nats-broker",level:2},{value:"Options",id:"options",level:3},{value:"nats_url",id:"nats_url",level:4},{value:"nats_prefix",id:"nats_prefix",level:4},{value:"nats_dial_timeout",id:"nats_dial_timeout",level:4},{value:"nats_write_timeout",id:"nats_write_timeout",level:4}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data."}),"\n",(0,i.jsx)(n.p,{children:"By default, Centrifugo uses a Memory engine. There are also Redis and Tarantool engines available. And Nats broker which also supports at most once PUB/SUB. Centrifugo also works with Redis-compatible storages such as AWS Elasticache, KeyDB, DragonflyDB."}),"\n",(0,i.jsx)(n.p,{children:"With default Memory engine you can start only one node of Centrifugo, while other engines allow running several nodes on different machines to scale client connections and for Centrifugo node high availability. In distributed case all Centrifugo nodes will be connected via broker PUB/SUB, will discover each other and deliver publications to the node where active channel subscribers exist."}),"\n",(0,i.jsx)(n.p,{children:"Memory engine keeps history and presence data in process memory, so the data is lost upon server restart. When using Redis Engine the data is kept in Redis (where you can configure desired persistence properties) instead of Centrifugo node process memory, so channel history data won't be lost after Centrifugo server restart."}),"\n",(0,i.jsxs)(n.p,{children:["To set engine you can use ",(0,i.jsx)(n.code,{children:"engine"})," configuration option. Available values are ",(0,i.jsx)(n.code,{children:"memory"}),", ",(0,i.jsx)(n.code,{children:"redis"}),", ",(0,i.jsx)(n.code,{children:"tarantool"}),". The default value is ",(0,i.jsx)(n.code,{children:"memory"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"For example to work with Redis engine:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis"\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"memory-engine",children:"Memory engine"}),"\n",(0,i.jsx)(n.p,{children:"Used by default. Supports only one node. Nice choice to start with. Supports all features keeping everything in Centrifugo node process memory. You don't need to install Redis when using this engine."}),"\n",(0,i.jsx)(n.p,{children:"Advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Super fast since it does not involve network at all"}),"\n",(0,i.jsx)(n.li,{children:"Does not require separate broker setup"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Disadvantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Does not allow scaling nodes (actually you still can scale Centrifugo with Memory engine but you have to publish data into each Centrifugo node and you won't have consistent history and presence state throughout Centrifugo nodes)"}),"\n",(0,i.jsx)(n.li,{children:"Does not persist message history in channels between Centrifugo restarts."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"redis-engine",children:"Redis engine"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://redis.io/",children:"Redis"})," is an open-source, in-memory data structure store, used as a database, cache, and message broker."]}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo Redis engine allows scaling Centrifugo nodes to different machines. Nodes will use Redis as a message broker (utilizing Redis PUB/SUB for node communication) and keep presence and history data in Redis."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Minimal Redis version is 5.0.1"})}),"\n",(0,i.jsx)(n.p,{children:"With Redis it's possible to come to the architecture like this:"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"redis",src:s(58061).Z+"",width:"2535",height:"1306"})}),"\n",(0,i.jsx)(n.h3,{id:"redis-engine-options",children:"Redis engine options"}),"\n",(0,i.jsx)(n.p,{children:"Several configuration options related to Redis engine."}),"\n",(0,i.jsx)(n.h4,{id:"redis_address",children:"redis_address"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'"127.0.0.1:6379"'})," - Redis server address."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_password",children:"redis_password"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," - Redis password."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_user",children:"redis_user"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," - Redis user for ",(0,i.jsx)(n.a,{href:"https://redis.io/docs/manual/security/acl/",children:"ACL-based"})," auth."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_db",children:"redis_db"}),"\n",(0,i.jsxs)(n.p,{children:["Integer, default ",(0,i.jsx)(n.code,{children:"0"})," - number of Redis db to use."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_prefix",children:"redis_prefix"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'"centrifugo"'})," \u2013 custom prefix to use for channels and keys in Redis."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_use_lists",children:"redis_use_lists"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," \u2013 turns on using Redis Lists instead of Stream data structure for keeping history (not recommended, keeping this for backwards compatibility mostly)."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_force_resp2",children:"redis_force_resp2"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"}),". If set to true it forces using RESP2 protocol for communicating with Redis. By default, Redis client used by Centrifugo tries to detect supported Redis protocol automatically trying RESP3 first."]}),"\n",(0,i.jsx)(n.h3,{id:"configuring-redis-tls",children:"Configuring Redis TLS"}),"\n",(0,i.jsx)(n.p,{children:"Some options may help you configuring TLS-protected communication between Centrifugo and Redis."}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls",children:"redis_tls"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - enable Redis TLS connection."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_insecure_skip_verify",children:"redis_tls_insecure_skip_verify"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - disable Redis TLS host verification. Centrifugo v4 also supports alias for this option \u2013 ",(0,i.jsx)(n.code,{children:"redis_tls_skip_verify"})," \u2013 but it will be removed in v5."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_cert",children:"redis_tls_cert"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS cert file. If you prefer passing certificate as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_cert_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_key",children:"redis_tls_key"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS key file. If you prefer passing cert key as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_key_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_root_ca",children:"redis_tls_root_ca"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS root CA file (in PEM format) to use. If you prefer passing root CA PEM as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_root_ca_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_server_name",children:"redis_tls_server_name"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 used to verify the hostname on the returned certificates. It is also included in the client's handshake to support virtual hosting unless it is an IP address."]}),"\n",(0,i.jsx)(n.h3,{id:"scaling-with-redis-tutorial",children:"Scaling with Redis tutorial"}),"\n",(0,i.jsx)(n.p,{children:"Let's see how to start several Centrifugo nodes using the Redis Engine. We will start 3 Centrifugo nodes and all those nodes will be connected via Redis."}),"\n",(0,i.jsx)(n.p,{children:"First, you should have Redis running. As soon as it's running - we can launch 3 Centrifugo instances. Open your terminal and start the first one:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8000 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsxs)(n.p,{children:["If your Redis is on the same machine and runs on its default port you can omit ",(0,i.jsx)(n.code,{children:"redis_address"})," option in the command above."]}),"\n",(0,i.jsx)(n.p,{children:"Then open another terminal and start another Centrifugo instance:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8001 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Note that we use another port number (",(0,i.jsx)(n.code,{children:"8001"}),") as port 8000 is already busy by our first Centrifugo instance. If you are starting Centrifugo instances on different machines then you most probably can use\nthe same port number (",(0,i.jsx)(n.code,{children:"8000"})," or whatever you want) for all instances."]}),"\n",(0,i.jsx)(n.p,{children:"And finally, let's start the third instance:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8002 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsx)(n.p,{children:"Now you have 3 Centrifugo instances running on ports 8000, 8001, 8002 and clients can connect to any of them. You can also send API requests to any of those nodes \u2013 as all nodes connected over Redis PUB/SUB message will be delivered to all interested clients on all nodes."}),"\n",(0,i.jsx)(n.p,{children:"To load balance clients between nodes you can use Nginx \u2013 you can find its configuration here in the documentation."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["In the production environment you will most probably run Centrifugo nodes on different hosts, so there will be no need to use different ",(0,i.jsx)(n.code,{children:"port"})," numbers."]})}),"\n",(0,i.jsx)(n.p,{children:"Here is a live example where we locally start two Centrifugo nodes both connected to local Redis:"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/redis_scale_example.mp4",type:"video/mp4"}),(0,i.jsx)(n.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(n.h3,{id:"redis-sentinel-for-high-availability",children:"Redis Sentinel for high availability"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo supports the official way to add high availability to Redis - Redis ",(0,i.jsx)(n.a,{href:"http://redis.io/topics/sentinel",children:"Sentinel"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["For this you only need to utilize 2 Redis Engine options: ",(0,i.jsx)(n.code,{children:"redis_sentinel_address"})," and ",(0,i.jsx)(n.code,{children:"redis_sentinel_master_name"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_address"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") - comma separated list of Sentinel addresses for HA. At least one known server required."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_master_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") - name of Redis master Sentinel monitors"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Also:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_password"})," \u2013 optional string password for your Sentinel, works with Redis Sentinel >= 5.0.1"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_user"})," - optional string user (used only in Redis ACL-based auth)."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"So you can start Centrifugo which will use Sentinels to discover Redis master instances like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json\n"})}),"\n",(0,i.jsx)(n.p,{children:"Where config.json:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_sentinel_address": "127.0.0.1:26379",\n "redis_sentinel_master_name": "mymaster"\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Sentinel configuration file may look like this (for 3-node Sentinel setup with quorum 2):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"port 26379\nsentinel monitor mymaster 127.0.0.1 6379 2\nsentinel down-after-milliseconds mymaster 10000\nsentinel failover-timeout mymaster 60000\n"})}),"\n",(0,i.jsxs)(n.p,{children:["You can find how to properly set up Sentinels ",(0,i.jsx)(n.a,{href:"http://redis.io/topics/sentinel",children:"in official documentation"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Note that when your Redis master instance is down there will be a small downtime interval until Sentinels\ndiscover a problem and come to a quorum decision about a new master. The length of this period depends on\nSentinel configuration."}),"\n",(0,i.jsx)(n.h3,{id:"redis-sentinel-tls",children:"Redis Sentinel TLS"}),"\n",(0,i.jsx)(n.p,{children:"To configure TLS for Redis Sentinel use the following options."}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls",children:"redis_sentinel_tls"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - enable Redis TLS connection."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_insecure_skip_verify",children:"redis_sentinel_tls_insecure_skip_verify"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - disable Redis TLS host verification. Centrifugo v4 also supports alias for this option \u2013 ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_skip_verify"})," \u2013 but it will be removed in v5."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_cert",children:"redis_sentinel_tls_cert"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS cert file. If you prefer passing certificate as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_cert_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_key",children:"redis_sentinel_tls_key"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS key file. If you prefer passing cert key as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_key_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_root_ca",children:"redis_sentinel_tls_root_ca"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS root CA file (in PEM format) to use. If you prefer passing root CA PEM as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_root_ca_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_server_name",children:"redis_sentinel_tls_server_name"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 used to verify the hostname on the returned certificates. It is also included in the client's handshake to support virtual hosting unless it is an IP address."]}),"\n",(0,i.jsx)(n.h3,{id:"haproxy-instead-of-sentinel-configuration",children:"Haproxy instead of Sentinel configuration"}),"\n",(0,i.jsx)(n.p,{children:"Alternatively, you can use Haproxy between Centrifugo and Redis to let it properly balance traffic to Redis master. In this case, you still need to configure Sentinels but you can omit Sentinel specifics from Centrifugo configuration and just use Redis address as in a simple non-HA case."}),"\n",(0,i.jsx)(n.p,{children:"For example, you can use something like this in Haproxy config:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"listen redis\n server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2\n server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup\n bind *:16379\n mode tcp\n option tcpka\n option tcplog\n option tcp-check\n tcp-check send PING\\r\\n\n tcp-check expect string +PONG\n tcp-check send info\\ replication\\r\\n\n tcp-check expect string role:master\n tcp-check send QUIT\\r\\n\n tcp-check expect string +OK\n balance roundrobin\n"})}),"\n",(0,i.jsx)(n.p,{children:"And then just point Centrifugo to this Haproxy:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'centrifugo --config=config.json --engine=redis --redis_address="localhost:16379"\n'})}),"\n",(0,i.jsx)(n.h3,{id:"redis-sharding",children:"Redis sharding"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo has built-in Redis sharding support."}),"\n",(0,i.jsx)(n.p,{children:"This resolves the situation when Redis becoming a bottleneck on a large Centrifugo setup. Redis is a single-threaded server, it's very fast but its power is not infinite so when your Redis approaches 100% CPU usage then the sharding feature can help your application to scale."}),"\n",(0,i.jsx)(n.p,{children:"At moment Centrifugo supports a simple comma-based approach to configuring Redis shards. Let's just look at examples."}),"\n",(0,i.jsx)(n.p,{children:"To start Centrifugo with 2 Redis shards on localhost running on port 6379 and port 6380 use config like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": [\n "127.0.0.1:6379",\n "127.0.0.1:6380",\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"To start Centrifugo with Redis instances on different hosts:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": [\n "192.168.1.34:6379",\n "192.168.1.35:6379",\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you also need to customize AUTH password, Redis DB number then you can use an extended address notation."}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsx)(n.p,{children:"Due to how Redis PUB/SUB works it's not possible (and it's pretty useless anyway) to run shards in one Redis instance using different Redis DB numbers."})}),"\n",(0,i.jsxs)(n.p,{children:["When sharding enabled Centrifugo will spread channels and history/presence keys over configured Redis instances using a consistent hashing algorithm. At moment we use Jump consistent hash algorithm (see ",(0,i.jsx)(n.a,{href:"https://arxiv.org/pdf/1406.2294.pdf",children:"paper"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/dgryski/go-jump",children:"implementation"}),")."]}),"\n",(0,i.jsx)(n.h3,{id:"redis-cluster",children:"Redis cluster"}),"\n",(0,i.jsxs)(n.p,{children:["Running Centrifugo with Redis cluster is simple and can be achieved using ",(0,i.jsx)(n.code,{children:"redis_cluster_address"})," option. This is an array of strings. Each element of the array is a comma-separated Redis cluster seed node. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "redis_cluster_address": [\n "localhost:30001,localhost:30002,localhost:30003"\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"You don't need to list all Redis cluster nodes in config \u2013 only several working nodes are enough to start."}),"\n",(0,i.jsx)(n.p,{children:"To set the same over environment variable:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'CENTRIFUGO_REDIS_CLUSTER_ADDRESS="localhost:30001" CENTRIFUGO_ENGINE=redis ./centrifugo\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you need to shard data between several Redis clusters then simply add one more string with seed nodes of another cluster to this array:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "redis_cluster_address": [\n "localhost:30001,localhost:30002,localhost:30003",\n "localhost:30101,localhost:30102,localhost:30103"\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Sharding between different Redis clusters can make sense due to the fact how PUB/SUB works in the Redis cluster. It does not scale linearly when adding nodes as all PUB/SUB messages got copied to every cluster node. See ",(0,i.jsx)(n.a,{href:"https://github.com/antirez/redis/issues/2672",children:"this discussion"})," for more information on topic. To spread data between different Redis clusters Centrifugo uses the same consistent hashing algorithm described above (i.e. ",(0,i.jsx)(n.code,{children:"Jump"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["To reproduce the same over environment variable use ",(0,i.jsx)(n.code,{children:"space"})," to separate different clusters:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'CENTRIFUGO_REDIS_CLUSTER_ADDRESS="localhost:30001,localhost:30002 localhost:30101,localhost:30102" CENTRIFUGO_ENGINE=redis ./centrifugo\n'})}),"\n",(0,i.jsx)(n.h3,{id:"optimize-getting-presence-stats",children:"Optimize getting presence stats"}),"\n",(0,i.jsxs)(n.p,{children:["Starting from Centrifugo v5.2.1 it's possible to keep user mapping information on Redis side to optimize ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence_stats",children:"presence stats"})," API."]}),"\n",(0,i.jsx)(n.p,{children:"It's implemented in a way that Centrifugo maintains additional per-user data structures in Redis. Similar to structures used for general client presence (ZSET + HASH). So we get a possibility to efficiently get both the number of clients in channel and the number of unique users in it."}),"\n",(0,i.jsx)(n.p,{children:"This may be useful to drastically reduce the time of Redis operation if you call presence stats for channels with large number of active subscribers. In our benchmarks, for a channel with 100k unique subscribers, number of presence stats ops bumped from 15 to 200k per second."}),"\n",(0,i.jsxs)(n.p,{children:["The feature comes with a cost \u2013 it increases memory usage in Redis, possibly up to 2x from what was spent on presence information before enabling (less if you use ",(0,i.jsx)(n.code,{children:"info"})," attached to a client connection, since Centrifugo does not include info payload to user mapping structures)."]}),"\n",(0,i.jsxs)(n.p,{children:["To enable set ",(0,i.jsx)(n.code,{children:"global_redis_presence_user_mapping"})," boolean option to ",(0,i.jsx)(n.code,{children:"true"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "global_redis_presence_user_mapping": true\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"other-redis-compatible",children:"Other Redis compatible"}),"\n",(0,i.jsx)(n.p,{children:"When using Redis engine it's possible to point Centrifugo not only to Redis itself, but also to the other Redis compatible server. Such servers may work just fine if implement Redis protocol and support all the data structures Centrifugo uses and have PUB/SUB implemented."}),"\n",(0,i.jsx)(n.p,{children:"Some known options:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://aws.amazon.com/elasticache/",children:"AWS Elasticache"})," \u2013 it was reported to work, but we suggest you testing the setup including failover tests and work under load."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://keydb.dev/",children:"KeyDB"})," \u2013 should work fine with Centrifugo, no known problems at this point regarding Centrifugo compatibility."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://dragonflydb.io/",children:"DragonflyDB"})," - should work fine (if you experience issues with it try enabling ",(0,i.jsx)(n.code,{children:"redis_force_resp2"})," option). We have not tested a Redis Cluster emulation mode provided by DragonflyDB yet. We suggest you testing the setup including failover tests and work under load."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/valkey-io/valkey",children:"Valkey"})," \u2013 should work fine since it's based on Redis v7, but no tests were performed by Centrifugal Labs."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"tarantool-engine",children:"Tarantool engine"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"DEPRECATED"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://www.tarantool.io",children:"Tarantool"})," is a fast and flexible in-memory storage with different persistence/replication schemes and LuaJIT for writing custom logic on the Tarantool side. It allows implementing Centrifugo engine with unique characteristics."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Tarantool engine is DEPRECATED and will be removed in Centrifugo v6. See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/830",children:"the issue on Github"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["There are many ways to operate Tarantool in production and it's hard to distribute Centrifugo Tarantool engine in a way that suits everyone. Centrifugo tries to fit generic case by providing ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," module and by providing ready-to-use ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/rotor",children:"centrifugal/rotor"})," project based on ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/tarantool/cartridge",children:"Tarantool Cartridge"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsx)(n.p,{children:"To be honest we bet on the community help to push this integration further. Tarantool provides an incredible performance boost for presence and history operations (up to 5x more RPS compared to the Redis Engine) and a pretty fast PUB/SUB (comparable to what Redis Engine provides). Let's see what we can build together."})}),"\n",(0,i.jsx)(n.p,{children:"There are several supported Tarantool topologies to which Centrifugo can connect:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"One standalone Tarantool instance"}),"\n",(0,i.jsx)(n.li,{children:"Many standalone Tarantool instances and consistently shard data between them"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool running in Cartridge"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool with replica and automatic failover in Cartridge"}),"\n",(0,i.jsx)(n.li,{children:"Many Tarantool instances (or leader-follower setup) in Cartridge with consistent client-side sharding between them"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool with synchronous replication (Raft-based, Tarantool >= 2.7)"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"After running Tarantool you can point Centrifugo to it (and of course scale Centrifugo nodes):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "tarantool",\n "tarantool_address": "127.0.0.1:3301"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/rotor",children:"centrifugal/rotor"})," repo for ready-to-use engine based on Tarantool Cartridge framework."]}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," repo for examples on how to run engine with Standalone single Tarantool instance or with Raft-based synchronous replication."]}),"\n",(0,i.jsx)(n.h3,{id:"tarantool-engine-options",children:"Tarantool engine options"}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_address",children:"tarantool_address"}),"\n",(0,i.jsxs)(n.p,{children:["String or array of strings. Default ",(0,i.jsx)(n.code,{children:"tcp://127.0.0.1:3301"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Connection address to Tarantool."}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_mode",children:"tarantool_mode"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"standalone"})]}),"\n",(0,i.jsxs)(n.p,{children:["A mode how to connect to Tarantool. Default is ",(0,i.jsx)(n.code,{children:"standalone"})," which connects to a single Tarantool instance address. Possible values are: ",(0,i.jsx)(n.code,{children:"leader-follower"})," (connects to a setup with Tarantool master and async replicas) and ",(0,i.jsx)(n.code,{children:"leader-follower-raft"})," (connects to a Tarantool with synchronous Raft-based replication)."]}),"\n",(0,i.jsx)(n.p,{children:"All modes support client-side consistent sharding (similar to what Redis engine provides)."}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_user",children:"tarantool_user"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'}),". Allows setting a user."]}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_password",children:"tarantool_password"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'}),". Allows setting a password."]}),"\n",(0,i.jsx)(n.h3,{id:"tarantool-engine-limitations",children:"Tarantool engine limitations"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"idempotent publish is not implemented"}),"\n",(0,i.jsx)(n.li,{children:"delta compression for connections with positioning/recovery on is not implemented"}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"nats-broker",children:"Nats broker"}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to scale with ",(0,i.jsx)(n.a,{href:"https://nats.io/",children:"Nats"})," PUB/SUB server. Keep in mind, that Nats is called a ",(0,i.jsx)(n.strong,{children:"broker"})," here, ",(0,i.jsx)(n.strong,{children:"not an Engine"})," \u2013 Nats integration only implements PUB/SUB part of Engine, so carefully read limitations below."]}),"\n",(0,i.jsx)(n.p,{children:"Limitations:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Nats integration works only for unreliable at most once PUB/SUB. This means that history, presence, and message recovery Centrifugo features won't be available."}),"\n",(0,i.jsxs)(n.li,{children:["Nats wildcard channel subscriptions with symbols ",(0,i.jsx)(n.code,{children:"*"})," and ",(0,i.jsx)(n.code,{children:">"})," not supported."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"First start Nats server:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"$ nats-server\n[3569] 2020/07/08 20:28:44.324269 [INF] Starting nats-server version 2.1.7\n[3569] 2020/07/08 20:28:44.324400 [INF] Git commit [not set]\n[3569] 2020/07/08 20:28:44.325600 [INF] Listening for client connections on 0.0.0.0:4222\n[3569] 2020/07/08 20:28:44.325612 [INF] Server id is NDAM7GEHUXAKS5SGMA3QE6ZSO4IQUJP6EL3G2E2LJYREVMAMIOBE7JT4\n[3569] 2020/07/08 20:28:44.325617 [INF] Server is ready\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then start Centrifugo with ",(0,i.jsx)(n.code,{children:"broker"})," option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"centrifugo --broker=nats --config=config.json\n"})}),"\n",(0,i.jsx)(n.p,{children:"And one more Centrifugo on another port (of course in real life you will start another Centrifugo on another machine):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"centrifugo --broker=nats --config=config.json --port=8001\n"})}),"\n",(0,i.jsx)(n.p,{children:"Now you can scale connections over Centrifugo instances, instances will be connected over Nats server."}),"\n",(0,i.jsx)(n.h3,{id:"options",children:"Options"}),"\n",(0,i.jsx)(n.h4,{id:"nats_url",children:"nats_url"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"nats://127.0.0.1:4222"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Connection url in format ",(0,i.jsx)(n.code,{children:"nats://derek:pass@localhost:4222"}),"."]}),"\n",(0,i.jsx)(n.h4,{id:"nats_prefix",children:"nats_prefix"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"centrifugo"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Prefix for channels used by Centrifugo inside Nats."}),"\n",(0,i.jsx)(n.h4,{id:"nats_dial_timeout",children:"nats_dial_timeout"}),"\n",(0,i.jsxs)(n.p,{children:["Duration, default ",(0,i.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Timeout for dialing with Nats."}),"\n",(0,i.jsx)(n.h4,{id:"nats_write_timeout",children:"nats_write_timeout"}),"\n",(0,i.jsxs)(n.p,{children:["Duration, default ",(0,i.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Write (and flush) timeout for a connection to Nats."})]})}function h(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},58061:(e,n,s)=>{s.d(n,{Z:()=>i});const i=s.p+"assets/images/redis_arch-2eed8366b3d81263ea9ec2201512f964.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>l,a:()=>o});var i=s(67294);const t={},r=i.createContext(t);function o(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5069],{47262:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var i=s(85893),t=s(11151);const r={id:"engines",title:"Engines and scalability"},o=void 0,l={id:"server/engines",title:"Engines and scalability",description:"The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data.",source:"@site/docs/server/engines.md",sourceDirName:"server",slug:"/server/engines",permalink:"/docs/server/engines",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/engines.md",tags:[],version:"current",frontMatter:{id:"engines",title:"Engines and scalability"},sidebar:"Guides",previous:{title:"Server-side subscriptions",permalink:"/docs/server/server_subs"},next:{title:"Async consumers",permalink:"/docs/server/consumers"}},a={},d=[{value:"Memory engine",id:"memory-engine",level:2},{value:"Redis engine",id:"redis-engine",level:2},{value:"Redis engine options",id:"redis-engine-options",level:3},{value:"redis_address",id:"redis_address",level:4},{value:"redis_password",id:"redis_password",level:4},{value:"redis_user",id:"redis_user",level:4},{value:"redis_db",id:"redis_db",level:4},{value:"redis_prefix",id:"redis_prefix",level:4},{value:"redis_use_lists",id:"redis_use_lists",level:4},{value:"redis_force_resp2",id:"redis_force_resp2",level:4},{value:"Configuring Redis TLS",id:"configuring-redis-tls",level:3},{value:"redis_tls",id:"redis_tls",level:4},{value:"redis_tls_insecure_skip_verify",id:"redis_tls_insecure_skip_verify",level:4},{value:"redis_tls_cert",id:"redis_tls_cert",level:4},{value:"redis_tls_key",id:"redis_tls_key",level:4},{value:"redis_tls_root_ca",id:"redis_tls_root_ca",level:4},{value:"redis_tls_server_name",id:"redis_tls_server_name",level:4},{value:"Scaling with Redis tutorial",id:"scaling-with-redis-tutorial",level:3},{value:"Redis Sentinel for high availability",id:"redis-sentinel-for-high-availability",level:3},{value:"Redis Sentinel TLS",id:"redis-sentinel-tls",level:3},{value:"redis_sentinel_tls",id:"redis_sentinel_tls",level:4},{value:"redis_sentinel_tls_insecure_skip_verify",id:"redis_sentinel_tls_insecure_skip_verify",level:4},{value:"redis_sentinel_tls_cert",id:"redis_sentinel_tls_cert",level:4},{value:"redis_sentinel_tls_key",id:"redis_sentinel_tls_key",level:4},{value:"redis_sentinel_tls_root_ca",id:"redis_sentinel_tls_root_ca",level:4},{value:"redis_sentinel_tls_server_name",id:"redis_sentinel_tls_server_name",level:4},{value:"Haproxy instead of Sentinel configuration",id:"haproxy-instead-of-sentinel-configuration",level:3},{value:"Redis sharding",id:"redis-sharding",level:3},{value:"Redis cluster",id:"redis-cluster",level:3},{value:"Optimize getting presence stats",id:"optimize-getting-presence-stats",level:3},{value:"Other Redis compatible",id:"other-redis-compatible",level:2},{value:"Tarantool engine",id:"tarantool-engine",level:2},{value:"Tarantool engine options",id:"tarantool-engine-options",level:3},{value:"tarantool_address",id:"tarantool_address",level:4},{value:"tarantool_mode",id:"tarantool_mode",level:4},{value:"tarantool_user",id:"tarantool_user",level:4},{value:"tarantool_password",id:"tarantool_password",level:4},{value:"Tarantool engine limitations",id:"tarantool-engine-limitations",level:3},{value:"Nats broker",id:"nats-broker",level:2},{value:"Options",id:"options",level:3},{value:"nats_url",id:"nats_url",level:4},{value:"nats_prefix",id:"nats_prefix",level:4},{value:"nats_dial_timeout",id:"nats_dial_timeout",level:4},{value:"nats_write_timeout",id:"nats_write_timeout",level:4}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"The Engine in Centrifugo is responsible for publishing messages between nodes, handle PUB/SUB broker subscriptions, save/retrieve online presence and history data."}),"\n",(0,i.jsx)(n.p,{children:"By default, Centrifugo uses a Memory engine. There are also Redis and Tarantool engines available. And Nats broker which also supports at most once PUB/SUB. Centrifugo also works with Redis-compatible storages such as AWS Elasticache, KeyDB, DragonflyDB."}),"\n",(0,i.jsx)(n.p,{children:"With default Memory engine you can start only one node of Centrifugo, while other engines allow running several nodes on different machines to scale client connections and for Centrifugo node high availability. In distributed case all Centrifugo nodes will be connected via broker PUB/SUB, will discover each other and deliver publications to the node where active channel subscribers exist."}),"\n",(0,i.jsx)(n.p,{children:"Memory engine keeps history and presence data in process memory, so the data is lost upon server restart. When using Redis Engine the data is kept in Redis (where you can configure desired persistence properties) instead of Centrifugo node process memory, so channel history data won't be lost after Centrifugo server restart."}),"\n",(0,i.jsxs)(n.p,{children:["To set engine you can use ",(0,i.jsx)(n.code,{children:"engine"})," configuration option. Available values are ",(0,i.jsx)(n.code,{children:"memory"}),", ",(0,i.jsx)(n.code,{children:"redis"}),", ",(0,i.jsx)(n.code,{children:"tarantool"}),". The default value is ",(0,i.jsx)(n.code,{children:"memory"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"For example to work with Redis engine:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis"\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"memory-engine",children:"Memory engine"}),"\n",(0,i.jsx)(n.p,{children:"Used by default. Supports only one node. Nice choice to start with. Supports all features keeping everything in Centrifugo node process memory. You don't need to install Redis when using this engine."}),"\n",(0,i.jsx)(n.p,{children:"Advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Super fast since it does not involve network at all"}),"\n",(0,i.jsx)(n.li,{children:"Does not require separate broker setup"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Disadvantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Does not allow scaling nodes (actually you still can scale Centrifugo with Memory engine but you have to publish data into each Centrifugo node and you won't have consistent history and presence state throughout Centrifugo nodes)"}),"\n",(0,i.jsx)(n.li,{children:"Does not persist message history in channels between Centrifugo restarts."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"redis-engine",children:"Redis engine"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://redis.io/",children:"Redis"})," is an open-source, in-memory data structure store, used as a database, cache, and message broker."]}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo Redis engine allows scaling Centrifugo nodes to different machines. Nodes will use Redis as a message broker (utilizing Redis PUB/SUB for node communication) and keep presence and history data in Redis."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Minimal Redis version is 5.0.1"})}),"\n",(0,i.jsx)(n.p,{children:"With Redis it's possible to come to the architecture like this:"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"redis",src:s(42621).Z+"",width:"2535",height:"1306"})}),"\n",(0,i.jsx)(n.h3,{id:"redis-engine-options",children:"Redis engine options"}),"\n",(0,i.jsx)(n.p,{children:"Several configuration options related to Redis engine."}),"\n",(0,i.jsx)(n.h4,{id:"redis_address",children:"redis_address"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'"127.0.0.1:6379"'})," - Redis server address."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_password",children:"redis_password"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," - Redis password."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_user",children:"redis_user"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," - Redis user for ",(0,i.jsx)(n.a,{href:"https://redis.io/docs/manual/security/acl/",children:"ACL-based"})," auth."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_db",children:"redis_db"}),"\n",(0,i.jsxs)(n.p,{children:["Integer, default ",(0,i.jsx)(n.code,{children:"0"})," - number of Redis db to use."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_prefix",children:"redis_prefix"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'"centrifugo"'})," \u2013 custom prefix to use for channels and keys in Redis."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_use_lists",children:"redis_use_lists"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," \u2013 turns on using Redis Lists instead of Stream data structure for keeping history (not recommended, keeping this for backwards compatibility mostly)."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_force_resp2",children:"redis_force_resp2"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"}),". If set to true it forces using RESP2 protocol for communicating with Redis. By default, Redis client used by Centrifugo tries to detect supported Redis protocol automatically trying RESP3 first."]}),"\n",(0,i.jsx)(n.h3,{id:"configuring-redis-tls",children:"Configuring Redis TLS"}),"\n",(0,i.jsx)(n.p,{children:"Some options may help you configuring TLS-protected communication between Centrifugo and Redis."}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls",children:"redis_tls"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - enable Redis TLS connection."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_insecure_skip_verify",children:"redis_tls_insecure_skip_verify"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - disable Redis TLS host verification. Centrifugo v4 also supports alias for this option \u2013 ",(0,i.jsx)(n.code,{children:"redis_tls_skip_verify"})," \u2013 but it will be removed in v5."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_cert",children:"redis_tls_cert"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS cert file. If you prefer passing certificate as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_cert_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_key",children:"redis_tls_key"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS key file. If you prefer passing cert key as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_key_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_root_ca",children:"redis_tls_root_ca"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS root CA file (in PEM format) to use. If you prefer passing root CA PEM as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_tls_root_ca_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_tls_server_name",children:"redis_tls_server_name"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 used to verify the hostname on the returned certificates. It is also included in the client's handshake to support virtual hosting unless it is an IP address."]}),"\n",(0,i.jsx)(n.h3,{id:"scaling-with-redis-tutorial",children:"Scaling with Redis tutorial"}),"\n",(0,i.jsx)(n.p,{children:"Let's see how to start several Centrifugo nodes using the Redis Engine. We will start 3 Centrifugo nodes and all those nodes will be connected via Redis."}),"\n",(0,i.jsx)(n.p,{children:"First, you should have Redis running. As soon as it's running - we can launch 3 Centrifugo instances. Open your terminal and start the first one:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8000 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsxs)(n.p,{children:["If your Redis is on the same machine and runs on its default port you can omit ",(0,i.jsx)(n.code,{children:"redis_address"})," option in the command above."]}),"\n",(0,i.jsx)(n.p,{children:"Then open another terminal and start another Centrifugo instance:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8001 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Note that we use another port number (",(0,i.jsx)(n.code,{children:"8001"}),") as port 8000 is already busy by our first Centrifugo instance. If you are starting Centrifugo instances on different machines then you most probably can use\nthe same port number (",(0,i.jsx)(n.code,{children:"8000"})," or whatever you want) for all instances."]}),"\n",(0,i.jsx)(n.p,{children:"And finally, let's start the third instance:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json --port=8002 --engine=redis --redis_address=127.0.0.1:6379\n"})}),"\n",(0,i.jsx)(n.p,{children:"Now you have 3 Centrifugo instances running on ports 8000, 8001, 8002 and clients can connect to any of them. You can also send API requests to any of those nodes \u2013 as all nodes connected over Redis PUB/SUB message will be delivered to all interested clients on all nodes."}),"\n",(0,i.jsx)(n.p,{children:"To load balance clients between nodes you can use Nginx \u2013 you can find its configuration here in the documentation."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["In the production environment you will most probably run Centrifugo nodes on different hosts, so there will be no need to use different ",(0,i.jsx)(n.code,{children:"port"})," numbers."]})}),"\n",(0,i.jsx)(n.p,{children:"Here is a live example where we locally start two Centrifugo nodes both connected to local Redis:"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/redis_scale_example.mp4",type:"video/mp4"}),(0,i.jsx)(n.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(n.h3,{id:"redis-sentinel-for-high-availability",children:"Redis Sentinel for high availability"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo supports the official way to add high availability to Redis - Redis ",(0,i.jsx)(n.a,{href:"http://redis.io/topics/sentinel",children:"Sentinel"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["For this you only need to utilize 2 Redis Engine options: ",(0,i.jsx)(n.code,{children:"redis_sentinel_address"})," and ",(0,i.jsx)(n.code,{children:"redis_sentinel_master_name"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_address"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") - comma separated list of Sentinel addresses for HA. At least one known server required."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_master_name"})," (string, default ",(0,i.jsx)(n.code,{children:'""'}),") - name of Redis master Sentinel monitors"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Also:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_password"})," \u2013 optional string password for your Sentinel, works with Redis Sentinel >= 5.0.1"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"redis_sentinel_user"})," - optional string user (used only in Redis ACL-based auth)."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"So you can start Centrifugo which will use Sentinels to discover Redis master instances like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"centrifugo --config=config.json\n"})}),"\n",(0,i.jsx)(n.p,{children:"Where config.json:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_sentinel_address": "127.0.0.1:26379",\n "redis_sentinel_master_name": "mymaster"\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Sentinel configuration file may look like this (for 3-node Sentinel setup with quorum 2):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"port 26379\nsentinel monitor mymaster 127.0.0.1 6379 2\nsentinel down-after-milliseconds mymaster 10000\nsentinel failover-timeout mymaster 60000\n"})}),"\n",(0,i.jsxs)(n.p,{children:["You can find how to properly set up Sentinels ",(0,i.jsx)(n.a,{href:"http://redis.io/topics/sentinel",children:"in official documentation"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Note that when your Redis master instance is down there will be a small downtime interval until Sentinels\ndiscover a problem and come to a quorum decision about a new master. The length of this period depends on\nSentinel configuration."}),"\n",(0,i.jsx)(n.h3,{id:"redis-sentinel-tls",children:"Redis Sentinel TLS"}),"\n",(0,i.jsx)(n.p,{children:"To configure TLS for Redis Sentinel use the following options."}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls",children:"redis_sentinel_tls"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - enable Redis TLS connection."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_insecure_skip_verify",children:"redis_sentinel_tls_insecure_skip_verify"}),"\n",(0,i.jsxs)(n.p,{children:["Boolean, default ",(0,i.jsx)(n.code,{children:"false"})," - disable Redis TLS host verification. Centrifugo v4 also supports alias for this option \u2013 ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_skip_verify"})," \u2013 but it will be removed in v5."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_cert",children:"redis_sentinel_tls_cert"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS cert file. If you prefer passing certificate as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_cert_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_key",children:"redis_sentinel_tls_key"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS key file. If you prefer passing cert key as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_key_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_root_ca",children:"redis_sentinel_tls_root_ca"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 path to TLS root CA file (in PEM format) to use. If you prefer passing root CA PEM as a string instead of path to the file then use ",(0,i.jsx)(n.code,{children:"redis_sentinel_tls_root_ca_pem"})," option."]}),"\n",(0,i.jsx)(n.h4,{id:"redis_sentinel_tls_server_name",children:"redis_sentinel_tls_server_name"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'})," \u2013 used to verify the hostname on the returned certificates. It is also included in the client's handshake to support virtual hosting unless it is an IP address."]}),"\n",(0,i.jsx)(n.h3,{id:"haproxy-instead-of-sentinel-configuration",children:"Haproxy instead of Sentinel configuration"}),"\n",(0,i.jsx)(n.p,{children:"Alternatively, you can use Haproxy between Centrifugo and Redis to let it properly balance traffic to Redis master. In this case, you still need to configure Sentinels but you can omit Sentinel specifics from Centrifugo configuration and just use Redis address as in a simple non-HA case."}),"\n",(0,i.jsx)(n.p,{children:"For example, you can use something like this in Haproxy config:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"listen redis\n server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2\n server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup\n bind *:16379\n mode tcp\n option tcpka\n option tcplog\n option tcp-check\n tcp-check send PING\\r\\n\n tcp-check expect string +PONG\n tcp-check send info\\ replication\\r\\n\n tcp-check expect string role:master\n tcp-check send QUIT\\r\\n\n tcp-check expect string +OK\n balance roundrobin\n"})}),"\n",(0,i.jsx)(n.p,{children:"And then just point Centrifugo to this Haproxy:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'centrifugo --config=config.json --engine=redis --redis_address="localhost:16379"\n'})}),"\n",(0,i.jsx)(n.h3,{id:"redis-sharding",children:"Redis sharding"}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo has built-in Redis sharding support."}),"\n",(0,i.jsx)(n.p,{children:"This resolves the situation when Redis becoming a bottleneck on a large Centrifugo setup. Redis is a single-threaded server, it's very fast but its power is not infinite so when your Redis approaches 100% CPU usage then the sharding feature can help your application to scale."}),"\n",(0,i.jsx)(n.p,{children:"At moment Centrifugo supports a simple comma-based approach to configuring Redis shards. Let's just look at examples."}),"\n",(0,i.jsx)(n.p,{children:"To start Centrifugo with 2 Redis shards on localhost running on port 6379 and port 6380 use config like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": [\n "127.0.0.1:6379",\n "127.0.0.1:6380",\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"To start Centrifugo with Redis instances on different hosts:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": [\n "192.168.1.34:6379",\n "192.168.1.35:6379",\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you also need to customize AUTH password, Redis DB number then you can use an extended address notation."}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsx)(n.p,{children:"Due to how Redis PUB/SUB works it's not possible (and it's pretty useless anyway) to run shards in one Redis instance using different Redis DB numbers."})}),"\n",(0,i.jsxs)(n.p,{children:["When sharding enabled Centrifugo will spread channels and history/presence keys over configured Redis instances using a consistent hashing algorithm. At moment we use Jump consistent hash algorithm (see ",(0,i.jsx)(n.a,{href:"https://arxiv.org/pdf/1406.2294.pdf",children:"paper"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/dgryski/go-jump",children:"implementation"}),")."]}),"\n",(0,i.jsx)(n.h3,{id:"redis-cluster",children:"Redis cluster"}),"\n",(0,i.jsxs)(n.p,{children:["Running Centrifugo with Redis cluster is simple and can be achieved using ",(0,i.jsx)(n.code,{children:"redis_cluster_address"})," option. This is an array of strings. Each element of the array is a comma-separated Redis cluster seed node. For example:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "redis_cluster_address": [\n "localhost:30001,localhost:30002,localhost:30003"\n ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"You don't need to list all Redis cluster nodes in config \u2013 only several working nodes are enough to start."}),"\n",(0,i.jsx)(n.p,{children:"To set the same over environment variable:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'CENTRIFUGO_REDIS_CLUSTER_ADDRESS="localhost:30001" CENTRIFUGO_ENGINE=redis ./centrifugo\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you need to shard data between several Redis clusters then simply add one more string with seed nodes of another cluster to this array:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "redis_cluster_address": [\n "localhost:30001,localhost:30002,localhost:30003",\n "localhost:30101,localhost:30102,localhost:30103"\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Sharding between different Redis clusters can make sense due to the fact how PUB/SUB works in the Redis cluster. It does not scale linearly when adding nodes as all PUB/SUB messages got copied to every cluster node. See ",(0,i.jsx)(n.a,{href:"https://github.com/antirez/redis/issues/2672",children:"this discussion"})," for more information on topic. To spread data between different Redis clusters Centrifugo uses the same consistent hashing algorithm described above (i.e. ",(0,i.jsx)(n.code,{children:"Jump"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["To reproduce the same over environment variable use ",(0,i.jsx)(n.code,{children:"space"})," to separate different clusters:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'CENTRIFUGO_REDIS_CLUSTER_ADDRESS="localhost:30001,localhost:30002 localhost:30101,localhost:30102" CENTRIFUGO_ENGINE=redis ./centrifugo\n'})}),"\n",(0,i.jsx)(n.h3,{id:"optimize-getting-presence-stats",children:"Optimize getting presence stats"}),"\n",(0,i.jsxs)(n.p,{children:["Starting from Centrifugo v5.2.1 it's possible to keep user mapping information on Redis side to optimize ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence_stats",children:"presence stats"})," API."]}),"\n",(0,i.jsx)(n.p,{children:"It's implemented in a way that Centrifugo maintains additional per-user data structures in Redis. Similar to structures used for general client presence (ZSET + HASH). So we get a possibility to efficiently get both the number of clients in channel and the number of unique users in it."}),"\n",(0,i.jsx)(n.p,{children:"This may be useful to drastically reduce the time of Redis operation if you call presence stats for channels with large number of active subscribers. In our benchmarks, for a channel with 100k unique subscribers, number of presence stats ops bumped from 15 to 200k per second."}),"\n",(0,i.jsxs)(n.p,{children:["The feature comes with a cost \u2013 it increases memory usage in Redis, possibly up to 2x from what was spent on presence information before enabling (less if you use ",(0,i.jsx)(n.code,{children:"info"})," attached to a client connection, since Centrifugo does not include info payload to user mapping structures)."]}),"\n",(0,i.jsxs)(n.p,{children:["To enable set ",(0,i.jsx)(n.code,{children:"global_redis_presence_user_mapping"})," boolean option to ",(0,i.jsx)(n.code,{children:"true"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",metastring:'title="config.json"',children:'{\n ...\n "global_redis_presence_user_mapping": true\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"other-redis-compatible",children:"Other Redis compatible"}),"\n",(0,i.jsx)(n.p,{children:"When using Redis engine it's possible to point Centrifugo not only to Redis itself, but also to the other Redis compatible server. Such servers may work just fine if implement Redis protocol and support all the data structures Centrifugo uses and have PUB/SUB implemented."}),"\n",(0,i.jsx)(n.p,{children:"Some known options:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://aws.amazon.com/elasticache/",children:"AWS Elasticache"})," \u2013 it was reported to work, but we suggest you testing the setup including failover tests and work under load."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://keydb.dev/",children:"KeyDB"})," \u2013 should work fine with Centrifugo, no known problems at this point regarding Centrifugo compatibility."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://dragonflydb.io/",children:"DragonflyDB"})," - should work fine (if you experience issues with it try enabling ",(0,i.jsx)(n.code,{children:"redis_force_resp2"})," option). We have not tested a Redis Cluster emulation mode provided by DragonflyDB yet. We suggest you testing the setup including failover tests and work under load."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/valkey-io/valkey",children:"Valkey"})," \u2013 should work fine since it's based on Redis v7, but no tests were performed by Centrifugal Labs."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"tarantool-engine",children:"Tarantool engine"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"DEPRECATED"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://www.tarantool.io",children:"Tarantool"})," is a fast and flexible in-memory storage with different persistence/replication schemes and LuaJIT for writing custom logic on the Tarantool side. It allows implementing Centrifugo engine with unique characteristics."]}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["Tarantool engine is DEPRECATED and will be removed in Centrifugo v6. See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/830",children:"the issue on Github"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["There are many ways to operate Tarantool in production and it's hard to distribute Centrifugo Tarantool engine in a way that suits everyone. Centrifugo tries to fit generic case by providing ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," module and by providing ready-to-use ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/rotor",children:"centrifugal/rotor"})," project based on ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," and ",(0,i.jsx)(n.a,{href:"https://github.com/tarantool/cartridge",children:"Tarantool Cartridge"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"info",children:(0,i.jsx)(n.p,{children:"To be honest we bet on the community help to push this integration further. Tarantool provides an incredible performance boost for presence and history operations (up to 5x more RPS compared to the Redis Engine) and a pretty fast PUB/SUB (comparable to what Redis Engine provides). Let's see what we can build together."})}),"\n",(0,i.jsx)(n.p,{children:"There are several supported Tarantool topologies to which Centrifugo can connect:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"One standalone Tarantool instance"}),"\n",(0,i.jsx)(n.li,{children:"Many standalone Tarantool instances and consistently shard data between them"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool running in Cartridge"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool with replica and automatic failover in Cartridge"}),"\n",(0,i.jsx)(n.li,{children:"Many Tarantool instances (or leader-follower setup) in Cartridge with consistent client-side sharding between them"}),"\n",(0,i.jsx)(n.li,{children:"Tarantool with synchronous replication (Raft-based, Tarantool >= 2.7)"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"After running Tarantool you can point Centrifugo to it (and of course scale Centrifugo nodes):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "tarantool",\n "tarantool_address": "127.0.0.1:3301"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/rotor",children:"centrifugal/rotor"})," repo for ready-to-use engine based on Tarantool Cartridge framework."]}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"https://github.com/centrifugal/tarantool-centrifuge",children:"centrifugal/tarantool-centrifuge"})," repo for examples on how to run engine with Standalone single Tarantool instance or with Raft-based synchronous replication."]}),"\n",(0,i.jsx)(n.h3,{id:"tarantool-engine-options",children:"Tarantool engine options"}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_address",children:"tarantool_address"}),"\n",(0,i.jsxs)(n.p,{children:["String or array of strings. Default ",(0,i.jsx)(n.code,{children:"tcp://127.0.0.1:3301"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Connection address to Tarantool."}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_mode",children:"tarantool_mode"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"standalone"})]}),"\n",(0,i.jsxs)(n.p,{children:["A mode how to connect to Tarantool. Default is ",(0,i.jsx)(n.code,{children:"standalone"})," which connects to a single Tarantool instance address. Possible values are: ",(0,i.jsx)(n.code,{children:"leader-follower"})," (connects to a setup with Tarantool master and async replicas) and ",(0,i.jsx)(n.code,{children:"leader-follower-raft"})," (connects to a Tarantool with synchronous Raft-based replication)."]}),"\n",(0,i.jsx)(n.p,{children:"All modes support client-side consistent sharding (similar to what Redis engine provides)."}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_user",children:"tarantool_user"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'}),". Allows setting a user."]}),"\n",(0,i.jsx)(n.h4,{id:"tarantool_password",children:"tarantool_password"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:'""'}),". Allows setting a password."]}),"\n",(0,i.jsx)(n.h3,{id:"tarantool-engine-limitations",children:"Tarantool engine limitations"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"idempotent publish is not implemented"}),"\n",(0,i.jsx)(n.li,{children:"delta compression for connections with positioning/recovery on is not implemented"}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"nats-broker",children:"Nats broker"}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to scale with ",(0,i.jsx)(n.a,{href:"https://nats.io/",children:"Nats"})," PUB/SUB server. Keep in mind, that Nats is called a ",(0,i.jsx)(n.strong,{children:"broker"})," here, ",(0,i.jsx)(n.strong,{children:"not an Engine"})," \u2013 Nats integration only implements PUB/SUB part of Engine, so carefully read limitations below."]}),"\n",(0,i.jsx)(n.p,{children:"Limitations:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Nats integration works only for unreliable at most once PUB/SUB. This means that history, presence, and message recovery Centrifugo features won't be available."}),"\n",(0,i.jsxs)(n.li,{children:["Nats wildcard channel subscriptions with symbols ",(0,i.jsx)(n.code,{children:"*"})," and ",(0,i.jsx)(n.code,{children:">"})," not supported."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"First start Nats server:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"$ nats-server\n[3569] 2020/07/08 20:28:44.324269 [INF] Starting nats-server version 2.1.7\n[3569] 2020/07/08 20:28:44.324400 [INF] Git commit [not set]\n[3569] 2020/07/08 20:28:44.325600 [INF] Listening for client connections on 0.0.0.0:4222\n[3569] 2020/07/08 20:28:44.325612 [INF] Server id is NDAM7GEHUXAKS5SGMA3QE6ZSO4IQUJP6EL3G2E2LJYREVMAMIOBE7JT4\n[3569] 2020/07/08 20:28:44.325617 [INF] Server is ready\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then start Centrifugo with ",(0,i.jsx)(n.code,{children:"broker"})," option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"centrifugo --broker=nats --config=config.json\n"})}),"\n",(0,i.jsx)(n.p,{children:"And one more Centrifugo on another port (of course in real life you will start another Centrifugo on another machine):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"centrifugo --broker=nats --config=config.json --port=8001\n"})}),"\n",(0,i.jsx)(n.p,{children:"Now you can scale connections over Centrifugo instances, instances will be connected over Nats server."}),"\n",(0,i.jsx)(n.h3,{id:"options",children:"Options"}),"\n",(0,i.jsx)(n.h4,{id:"nats_url",children:"nats_url"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"nats://127.0.0.1:4222"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Connection url in format ",(0,i.jsx)(n.code,{children:"nats://derek:pass@localhost:4222"}),"."]}),"\n",(0,i.jsx)(n.h4,{id:"nats_prefix",children:"nats_prefix"}),"\n",(0,i.jsxs)(n.p,{children:["String, default ",(0,i.jsx)(n.code,{children:"centrifugo"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Prefix for channels used by Centrifugo inside Nats."}),"\n",(0,i.jsx)(n.h4,{id:"nats_dial_timeout",children:"nats_dial_timeout"}),"\n",(0,i.jsxs)(n.p,{children:["Duration, default ",(0,i.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Timeout for dialing with Nats."}),"\n",(0,i.jsx)(n.h4,{id:"nats_write_timeout",children:"nats_write_timeout"}),"\n",(0,i.jsxs)(n.p,{children:["Duration, default ",(0,i.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Write (and flush) timeout for a connection to Nats."})]})}function h(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},42621:(e,n,s)=>{s.d(n,{Z:()=>i});const i=s.p+"assets/images/redis_arch-2eed8366b3d81263ea9ec2201512f964.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>l,a:()=>o});var i=s(67294);const t={},r=i.createContext(t);function o(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/19e7756f.60e8dc46.js b/assets/js/19e7756f.60e8dc46.js new file mode 100644 index 000000000..5ebbb4fa1 --- /dev/null +++ b/assets/js/19e7756f.60e8dc46.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7810],{9528:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>g,frontMatter:()=>a,metadata:()=>h,toc:()=>u});var i=t(85893),o=t(11151),r=t(67294),s=t(9286);class c extends r.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={providerKey:"",centrifugalKey:""}}async exchangeLicense(e){const n=await fetch("https://centrifugal.fly.dev/centrifugo/license/exchange/"+this.props.providerName+"?license="+e);if(!n.ok)throw new Error(`Unexpected status code ${n.status}`);const t=await n.json();this.setState({centrifugalKey:t.license,providerKey:""})}onClick(e){this.state.providerKey?this.exchangeLicense(this.state.providerKey):alert("Provide a license key received in the purchase confirmation on email")}onChange(e){this.setState({providerKey:e.target.value})}render(){const e="Paste the key received from "+this.props.providerHuman+" here...";return(0,i.jsxs)("div",{children:[(0,i.jsx)("input",{onChange:this.onChange,value:this.state.providerKey,placeholder:e,style:{backgroundColor:"#230808",color:"#ccc",width:"100%",height:"3em",border:"1px solid #ccc",padding:"5px",fontSize:"1em",borderRadius:"5px"}}),(0,i.jsx)("button",{onClick:this.onClick,style:{background:"#FC6459",height:50,border:"none",textAlign:"center",cursor:"pointer",textTransform:"uppercase",outline:"none",overflow:"hidden",position:"relative",color:"#fff",fontWeight:700,fontSize:15,padding:"17px 17px",marginTop:10,borderRadius:"5px"},children:"Exchange"}),this.state.centrifugalKey&&(0,i.jsx)("div",{style:{marginTop:"10px"},children:(0,i.jsx)(s.Z,{language:"text",children:this.state.centrifugalKey})})]})}}const a={id:"license_lemon",title:"Getting Centrifugo PRO license",hide_table_of_contents:!0},l=void 0,h={type:"mdx",permalink:"/license_exchange_lemon",source:"@site/src/pages/license_exchange_lemon.md",title:"Getting Centrifugo PRO license",description:"Thanks for purchasing Centrifugo PRO \ud83c\udf89",frontMatter:{id:"license_lemon",title:"Getting Centrifugo PRO license",hide_table_of_contents:!0},unlisted:!1},d={},u=[{value:"Thanks for purchasing Centrifugo PRO \ud83c\udf89",id:"thanks-for-purchasing-centrifugo-pro-",level:2}];function p(e){const n={h2:"h2",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h2,{id:"thanks-for-purchasing-centrifugo-pro-",children:"Thanks for purchasing Centrifugo PRO \ud83c\udf89"}),"\n",(0,i.jsx)(n.p,{children:"In the email you received from Lemon Squeeze you can find the license key. You need to exchange it to Centrifugo license key using the form below. Please paste the license key from the Lemon Squeeze email to the input below, press Exchange button \u2013 and we will exchange the Lemon Squezee license key to a Centrifugo PRO license key."}),"\n","\n",(0,i.jsx)(c,{providerName:"lemon",providerHuman:"Lemon Squezee"})]})}function g(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(p,{...e})}):p(e)}}}]); \ No newline at end of file diff --git a/assets/js/19e7756f.731b1131.js b/assets/js/19e7756f.731b1131.js deleted file mode 100644 index fd89cf5ee..000000000 --- a/assets/js/19e7756f.731b1131.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7810],{75896:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>g,frontMatter:()=>a,metadata:()=>h,toc:()=>u});var i=t(85893),o=t(11151),r=t(67294),s=t(84316);class c extends r.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={providerKey:"",centrifugalKey:""}}async exchangeLicense(e){const n=await fetch("https://centrifugal.fly.dev/centrifugo/license/exchange/"+this.props.providerName+"?license="+e);if(!n.ok)throw new Error(`Unexpected status code ${n.status}`);const t=await n.json();this.setState({centrifugalKey:t.license,providerKey:""})}onClick(e){this.state.providerKey?this.exchangeLicense(this.state.providerKey):alert("Provide a license key received in the purchase confirmation on email")}onChange(e){this.setState({providerKey:e.target.value})}render(){const e="Paste the key received from "+this.props.providerHuman+" here...";return(0,i.jsxs)("div",{children:[(0,i.jsx)("input",{onChange:this.onChange,value:this.state.providerKey,placeholder:e,style:{backgroundColor:"#230808",color:"#ccc",width:"100%",height:"3em",border:"1px solid #ccc",padding:"5px",fontSize:"1em",borderRadius:"5px"}}),(0,i.jsx)("button",{onClick:this.onClick,style:{background:"#FC6459",height:50,border:"none",textAlign:"center",cursor:"pointer",textTransform:"uppercase",outline:"none",overflow:"hidden",position:"relative",color:"#fff",fontWeight:700,fontSize:15,padding:"17px 17px",marginTop:10,borderRadius:"5px"},children:"Exchange"}),this.state.centrifugalKey&&(0,i.jsx)("div",{style:{marginTop:"10px"},children:(0,i.jsx)(s.Z,{language:"text",children:this.state.centrifugalKey})})]})}}const a={id:"license_lemon",title:"Getting Centrifugo PRO license",hide_table_of_contents:!0},l=void 0,h={type:"mdx",permalink:"/license_exchange_lemon",source:"@site/src/pages/license_exchange_lemon.md",title:"Getting Centrifugo PRO license",description:"Thanks for purchasing Centrifugo PRO \ud83c\udf89",frontMatter:{id:"license_lemon",title:"Getting Centrifugo PRO license",hide_table_of_contents:!0},unlisted:!1},d={},u=[{value:"Thanks for purchasing Centrifugo PRO \ud83c\udf89",id:"thanks-for-purchasing-centrifugo-pro-",level:2}];function p(e){const n={h2:"h2",p:"p",...(0,o.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h2,{id:"thanks-for-purchasing-centrifugo-pro-",children:"Thanks for purchasing Centrifugo PRO \ud83c\udf89"}),"\n",(0,i.jsx)(n.p,{children:"In the email you received from Lemon Squeeze you can find the license key. You need to exchange it to Centrifugo license key using the form below. Please paste the license key from the Lemon Squeeze email to the input below, press Exchange button \u2013 and we will exchange the Lemon Squezee license key to a Centrifugo PRO license key."}),"\n","\n",(0,i.jsx)(c,{providerName:"lemon",providerHuman:"Lemon Squezee"})]})}function g(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(p,{...e})}):p(e)}}}]); \ No newline at end of file diff --git a/assets/js/1a4e3797.7620272a.js b/assets/js/1a4e3797.7620272a.js deleted file mode 100644 index 8955fc744..000000000 --- a/assets/js/1a4e3797.7620272a.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see 1a4e3797.7620272a.js.LICENSE.txt */ -(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7920],{17331:e=>{function t(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function r(e){return"function"==typeof e}function n(e){return"object"==typeof e&&null!==e}function i(e){return void 0===e}e.exports=t,t.prototype._events=void 0,t.prototype._maxListeners=void 0,t.defaultMaxListeners=10,t.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},t.prototype.emit=function(e){var t,s,a,c,u,o;if(this._events||(this._events={}),"error"===e&&(!this._events.error||n(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var h=new Error('Uncaught, unspecified "error" event. ('+t+")");throw h.context=t,h}if(i(s=this._events[e]))return!1;if(r(s))switch(arguments.length){case 1:s.call(this);break;case 2:s.call(this,arguments[1]);break;case 3:s.call(this,arguments[1],arguments[2]);break;default:c=Array.prototype.slice.call(arguments,1),s.apply(this,c)}else if(n(s))for(c=Array.prototype.slice.call(arguments,1),a=(o=s.slice()).length,u=0;u<a;u++)o[u].apply(this,c);return!0},t.prototype.addListener=function(e,s){var a;if(!r(s))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,r(s.listener)?s.listener:s),this._events[e]?n(this._events[e])?this._events[e].push(s):this._events[e]=[this._events[e],s]:this._events[e]=s,n(this._events[e])&&!this._events[e].warned&&(a=i(this._maxListeners)?t.defaultMaxListeners:this._maxListeners)&&a>0&&this._events[e].length>a&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},t.prototype.on=t.prototype.addListener,t.prototype.once=function(e,t){if(!r(t))throw TypeError("listener must be a function");var n=!1;function i(){this.removeListener(e,i),n||(n=!0,t.apply(this,arguments))}return i.listener=t,this.on(e,i),this},t.prototype.removeListener=function(e,t){var i,s,a,c;if(!r(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(a=(i=this._events[e]).length,s=-1,i===t||r(i.listener)&&i.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(n(i)){for(c=a;c-- >0;)if(i[c]===t||i[c].listener&&i[c].listener===t){s=c;break}if(s<0)return this;1===i.length?(i.length=0,delete this._events[e]):i.splice(s,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},t.prototype.removeAllListeners=function(e){var t,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r(n=this._events[e]))this.removeListener(e,n);else if(n)for(;n.length;)this.removeListener(e,n[n.length-1]);return delete this._events[e],this},t.prototype.listeners=function(e){return this._events&&this._events[e]?r(this._events[e])?[this._events[e]]:this._events[e].slice():[]},t.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(r(t))return 1;if(t)return t.length}return 0},t.listenerCount=function(e,t){return e.listenerCount(t)}},57880:(e,t,r)=>{"use strict";r.d(t,{c:()=>o});var n=r(67294),i=r(6832);const s=["zero","one","two","few","many","other"];function a(e){return s.filter((t=>e.includes(t)))}const c={locale:"en",pluralForms:a(["one","other"]),select:e=>1===e?"one":"other"};function u(){const{i18n:{currentLocale:e}}=(0,i.Z)();return(0,n.useMemo)((()=>{try{return function(e){const t=new Intl.PluralRules(e);return{locale:e,pluralForms:a(t.resolvedOptions().pluralCategories),select:e=>t.select(e)}}(e)}catch(t){return console.error(`Failed to use Intl.PluralRules for locale "${e}".\nDocusaurus will fallback to the default (English) implementation.\nError: ${t.message}\n`),c}}),[e])}function o(){const e=u();return{selectMessage:(t,r)=>function(e,t,r){const n=e.split("|");if(1===n.length)return n[0];n.length>r.pluralForms.length&&console.error(`For locale=${r.locale}, a maximum of ${r.pluralForms.length} plural forms are expected (${r.pluralForms.join(",")}), but the message contains ${n.length}: ${e}`);const i=r.select(t),s=r.pluralForms.indexOf(i);return n[Math.min(s,n.length-1)]}(r,t,e)}}},19291:(e,t,r)=>{"use strict";r.r(t),r.d(t,{default:()=>A});var n=r(67294);function i(e){var t,r,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var s=e.length;for(t=0;t<s;t++)e[t]&&(r=i(e[t]))&&(n&&(n+=" "),n+=r)}else for(r in e)e[r]&&(n&&(n+=" "),n+=r);return n}const s=function(){for(var e,t,r=0,n="",s=arguments.length;r<s;r++)(e=arguments[r])&&(t=i(e))&&(n&&(n+=" "),n+=t);return n};var a=r(8131),c=r.n(a),u=r(70290),o=r.n(u),h=r(19901),f=r(32411),l=r(75013),m=r(4452),d=r(57880),p=r(9512),v=r(93478),g=r(62581),y=r(71427),R=r(11614),F=r(6832),b=r(12057),j=r(80180),P=r(78299),x=r(34055);const _={searchQueryInput:"searchQueryInput_u2C7",searchVersionInput:"searchVersionInput_m0Ui",searchResultsColumn:"searchResultsColumn_JPFH",algoliaLogo:"algoliaLogo_rT1R",algoliaLogoPathFill:"algoliaLogoPathFill_WdUC",searchResultItem:"searchResultItem_Tv2o",searchResultItemHeading:"searchResultItemHeading_KbCB",searchResultItemPath:"searchResultItemPath_lhe1",searchResultItemSummary:"searchResultItemSummary_AEaO",searchQueryColumn:"searchQueryColumn_RTkw",searchVersionColumn:"searchVersionColumn_ypXd",searchLogoColumn:"searchLogoColumn_rJIA",loadingSpinner:"loadingSpinner_XVxU","loading-spin":"loading-spin_vzvp",loader:"loader_vvXV"};var E=r(85893);function O(e){let{docsSearchVersionsHelpers:t}=e;const r=Object.entries(t.allDocsData).filter((e=>{let[,t]=e;return t.versions.length>1}));return(0,E.jsx)("div",{className:s("col","col--3","padding-left--none",_.searchVersionColumn),children:r.map((e=>{let[n,i]=e;const s=r.length>1?`${n}: `:"";return(0,E.jsx)("select",{onChange:e=>t.setSearchVersion(n,e.target.value),defaultValue:t.searchVersions[n],className:_.searchVersionInput,children:i.versions.map(((e,t)=>(0,E.jsx)("option",{label:`${s}${e.label}`,value:e.name},t)))},n)}))})}function w(){const{i18n:{currentLocale:e}}=(0,F.Z)(),{algolia:{appId:t,apiKey:r,indexName:i}}=(0,b.L)(),a=(0,j.l)(),u=function(){const{selectMessage:e}=(0,d.c)();return t=>e(t,(0,R.I)({id:"theme.SearchPage.documentsFound.plurals",description:'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',message:"One document found|{count} documents found"},{count:t}))}(),g=function(){const e=(0,m._r)(),[t,r]=(0,n.useState)((()=>Object.entries(e).reduce(((e,t)=>{let[r,n]=t;return{...e,[r]:n.versions[0].name}}),{}))),i=Object.values(e).some((e=>e.versions.length>1));return{allDocsData:e,versioningEnabled:i,searchVersions:t,setSearchVersion:(e,t)=>r((r=>({...r,[e]:t})))}}(),[w,A]=(0,p.K)(),N={items:[],query:null,totalResults:null,totalPages:null,lastPage:null,hasMore:null,loading:null},[H,S]=(0,n.useReducer)(((e,t)=>{switch(t.type){case"reset":return N;case"loading":return{...e,loading:!0};case"update":return w!==t.value.query?e:{...t.value,items:0===t.value.lastPage?t.value.items:e.items.concat(t.value.items)};case"advance":{const t=e.totalPages>e.lastPage+1;return{...e,lastPage:t?e.lastPage+1:e.lastPage,hasMore:t}}default:return e}}),N),T=o()(t,r),Q=c()(T,i,{hitsPerPage:15,advancedSyntax:!0,disjunctiveFacets:["language","docusaurus_tag"]});Q.on("result",(e=>{let{results:{query:t,hits:r,page:n,nbHits:i,nbPages:s}}=e;if(""===t||!Array.isArray(r))return void S({type:"reset"});const c=e=>e.replace(/algolia-docsearch-suggestion--highlight/g,"search-result-match"),u=r.map((e=>{let{url:t,_highlightResult:{hierarchy:r},_snippetResult:n={}}=e;const i=Object.keys(r).map((e=>c(r[e].value)));return{title:i.pop(),url:a(t),summary:n.content?`${c(n.content.value)}...`:"",breadcrumbs:i}}));S({type:"update",value:{items:u,query:t,totalResults:i,totalPages:s,lastPage:n,hasMore:s>n+1,loading:!1}})}));const[C,I]=(0,n.useState)(null),D=(0,n.useRef)(0),k=(0,n.useRef)(h.Z.canUseIntersectionObserver&&new IntersectionObserver((e=>{const{isIntersecting:t,boundingClientRect:{y:r}}=e[0];t&&D.current>r&&S({type:"advance"}),D.current=r}),{threshold:1})),q=()=>w?(0,R.I)({id:"theme.SearchPage.existingResultsTitle",message:'Search results for "{query}"',description:"The search page title for non-empty query"},{query:w}):(0,R.I)({id:"theme.SearchPage.emptyResultsTitle",message:"Search the documentation",description:"The search page title for empty query"}),V=(0,v.zX)((function(t){void 0===t&&(t=0),Q.addDisjunctiveFacetRefinement("docusaurus_tag","default"),Q.addDisjunctiveFacetRefinement("language",e),Object.entries(g.searchVersions).forEach((e=>{let[t,r]=e;Q.addDisjunctiveFacetRefinement("docusaurus_tag",`docs-${t}-${r}`)})),Q.setQuery(w).setPage(t).search()}));return(0,n.useEffect)((()=>{if(!C)return;const e=k.current;return e?(e.observe(C),()=>e.unobserve(C)):()=>!0}),[C]),(0,n.useEffect)((()=>{S({type:"reset"}),w&&(S({type:"loading"}),setTimeout((()=>{V()}),300))}),[w,g.searchVersions,V]),(0,n.useEffect)((()=>{H.lastPage&&0!==H.lastPage&&V(H.lastPage)}),[V,H.lastPage]),(0,E.jsxs)(P.Z,{children:[(0,E.jsxs)(f.Z,{children:[(0,E.jsx)("title",{children:(0,y.p)(q())}),(0,E.jsx)("meta",{property:"robots",content:"noindex, follow"})]}),(0,E.jsxs)("div",{className:"container margin-vert--lg",children:[(0,E.jsx)(x.Z,{as:"h1",children:q()}),(0,E.jsxs)("form",{className:"row",onSubmit:e=>e.preventDefault(),children:[(0,E.jsx)("div",{className:s("col",_.searchQueryColumn,{"col--9":g.versioningEnabled,"col--12":!g.versioningEnabled}),children:(0,E.jsx)("input",{type:"search",name:"q",className:_.searchQueryInput,placeholder:(0,R.I)({id:"theme.SearchPage.inputPlaceholder",message:"Type your search here",description:"The placeholder for search page input"}),"aria-label":(0,R.I)({id:"theme.SearchPage.inputLabel",message:"Search",description:"The ARIA label for search page input"}),onChange:e=>A(e.target.value),value:w,autoComplete:"off",autoFocus:!0})}),g.versioningEnabled&&(0,E.jsx)(O,{docsSearchVersionsHelpers:g})]}),(0,E.jsxs)("div",{className:"row",children:[(0,E.jsx)("div",{className:s("col","col--8",_.searchResultsColumn),children:!!H.totalResults&&u(H.totalResults)}),(0,E.jsx)("div",{className:s("col","col--4","text--right",_.searchLogoColumn),children:(0,E.jsx)(l.Z,{to:"https://www.algolia.com/","aria-label":(0,R.I)({id:"theme.SearchPage.algoliaLabel",message:"Search by Algolia",description:"The ARIA label for Algolia mention"}),children:(0,E.jsx)("svg",{viewBox:"0 0 168 24",className:_.algoliaLogo,children:(0,E.jsxs)("g",{fill:"none",children:[(0,E.jsx)("path",{className:_.algoliaLogoPathFill,d:"M120.925 18.804c-4.386.02-4.386-3.54-4.386-4.106l-.007-13.336 2.675-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-10.846-2.18c.821 0 1.43-.047 1.855-.129v-2.719a6.334 6.334 0 0 0-1.574-.199 5.7 5.7 0 0 0-.897.069 2.699 2.699 0 0 0-.814.24c-.24.116-.439.28-.582.491-.15.212-.219.335-.219.656 0 .628.219.991.616 1.23s.938.362 1.615.362zm-.233-9.7c.883 0 1.629.109 2.231.328.602.218 1.088.525 1.444.915.363.396.609.922.76 1.483.157.56.232 1.175.232 1.85v6.874a32.5 32.5 0 0 1-1.868.314c-.834.123-1.772.185-2.813.185-.69 0-1.327-.069-1.895-.198a4.001 4.001 0 0 1-1.471-.636 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.803 0-.656.13-1.073.384-1.525a3.24 3.24 0 0 1 1.047-1.106c.445-.287.95-.492 1.532-.615a8.8 8.8 0 0 1 1.82-.185 8.404 8.404 0 0 1 1.972.24v-.438c0-.307-.035-.6-.11-.874a1.88 1.88 0 0 0-.384-.73 1.784 1.784 0 0 0-.724-.493 3.164 3.164 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.735 7.735 0 0 0-1.26.307l-.321-2.192c.335-.117.834-.233 1.478-.349a10.98 10.98 0 0 1 2.073-.178zm52.842 9.626c.822 0 1.43-.048 1.854-.13V13.7a6.347 6.347 0 0 0-1.574-.199c-.294 0-.595.021-.896.069a2.7 2.7 0 0 0-.814.24 1.46 1.46 0 0 0-.582.491c-.15.212-.218.335-.218.656 0 .628.218.991.615 1.23.404.245.938.362 1.615.362zm-.226-9.694c.883 0 1.629.108 2.231.327.602.219 1.088.526 1.444.915.355.39.609.923.759 1.483a6.8 6.8 0 0 1 .233 1.852v6.873c-.41.088-1.034.19-1.868.314-.834.123-1.772.184-2.813.184-.69 0-1.327-.068-1.895-.198a4.001 4.001 0 0 1-1.471-.635 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.804 0-.656.13-1.073.384-1.524.26-.45.608-.82 1.047-1.107.445-.286.95-.491 1.532-.614a8.803 8.803 0 0 1 2.751-.13c.329.034.671.096 1.04.185v-.437a3.3 3.3 0 0 0-.109-.875 1.873 1.873 0 0 0-.384-.731 1.784 1.784 0 0 0-.724-.492 3.165 3.165 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.75 7.75 0 0 0-1.26.307l-.321-2.193c.335-.116.834-.232 1.478-.348a11.633 11.633 0 0 1 2.073-.177zm-8.034-1.271a1.626 1.626 0 0 1-1.628-1.62c0-.895.725-1.62 1.628-1.62.904 0 1.63.725 1.63 1.62 0 .895-.733 1.62-1.63 1.62zm1.348 13.22h-2.689V7.27l2.69-.423v11.956zm-4.714 0c-4.386.02-4.386-3.54-4.386-4.107l-.008-13.336 2.676-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-8.698-5.903c0-1.156-.253-2.119-.746-2.788-.493-.677-1.183-1.01-2.067-1.01-.882 0-1.574.333-2.065 1.01-.493.676-.733 1.632-.733 2.788 0 1.168.246 1.953.74 2.63.492.683 1.183 1.018 2.066 1.018.882 0 1.574-.342 2.067-1.019.492-.683.738-1.46.738-2.63zm2.737-.007c0 .902-.13 1.584-.397 2.33a5.52 5.52 0 0 1-1.128 1.906 4.986 4.986 0 0 1-1.752 1.223c-.685.286-1.739.45-2.265.45-.528-.006-1.574-.157-2.252-.45a5.096 5.096 0 0 1-1.744-1.223c-.487-.527-.863-1.162-1.137-1.906a6.345 6.345 0 0 1-.41-2.33c0-.902.123-1.77.397-2.508a5.554 5.554 0 0 1 1.15-1.892 5.133 5.133 0 0 1 1.75-1.216c.679-.287 1.425-.423 2.232-.423.808 0 1.553.142 2.237.423a4.88 4.88 0 0 1 1.753 1.216 5.644 5.644 0 0 1 1.135 1.892c.287.738.431 1.606.431 2.508zm-20.138 0c0 1.12.246 2.363.738 2.882.493.52 1.13.78 1.91.78.424 0 .828-.062 1.204-.178.377-.116.677-.253.917-.417V9.33a10.476 10.476 0 0 0-1.766-.226c-.971-.028-1.71.37-2.23 1.004-.513.636-.773 1.75-.773 2.788zm7.438 5.274c0 1.824-.466 3.156-1.404 4.004-.936.846-2.367 1.27-4.296 1.27-.705 0-2.17-.137-3.34-.396l.431-2.118c.98.205 2.272.26 2.95.26 1.074 0 1.84-.219 2.299-.656.459-.437.684-1.086.684-1.948v-.437a8.07 8.07 0 0 1-1.047.397c-.43.13-.93.198-1.492.198-.739 0-1.41-.116-2.018-.349a4.206 4.206 0 0 1-1.567-1.025c-.431-.45-.774-1.017-1.013-1.694-.24-.677-.363-1.885-.363-2.773 0-.834.13-1.88.384-2.577.26-.696.629-1.298 1.129-1.796.493-.498 1.095-.881 1.8-1.162a6.605 6.605 0 0 1 2.428-.457c.87 0 1.67.109 2.45.24.78.129 1.444.265 1.985.415V18.17zM6.972 6.677v1.627c-.712-.446-1.52-.67-2.425-.67-.585 0-1.045.13-1.38.391a1.24 1.24 0 0 0-.502 1.03c0 .425.164.765.494 1.02.33.256.835.532 1.516.83.447.192.795.356 1.045.495.25.138.537.332.862.582.324.25.563.548.718.894.154.345.23.741.23 1.188 0 .947-.334 1.691-1.004 2.234-.67.542-1.537.814-2.601.814-1.18 0-2.16-.229-2.936-.686v-1.708c.84.628 1.814.942 2.92.942.585 0 1.048-.136 1.388-.407.34-.271.51-.646.51-1.125 0-.287-.1-.55-.302-.79-.203-.24-.42-.42-.655-.542-.234-.123-.585-.29-1.053-.503a61.27 61.27 0 0 1-.582-.271 13.67 13.67 0 0 1-.55-.287 4.275 4.275 0 0 1-.567-.351 6.92 6.92 0 0 1-.455-.4c-.18-.17-.31-.34-.39-.51-.08-.17-.155-.37-.224-.598a2.553 2.553 0 0 1-.104-.742c0-.915.333-1.638.998-2.17.664-.532 1.523-.798 2.576-.798.968 0 1.793.17 2.473.51zm7.468 5.696v-.287c-.022-.607-.187-1.088-.495-1.444-.309-.357-.75-.535-1.324-.535-.532 0-.99.194-1.373.583-.382.388-.622.949-.717 1.683h3.909zm1.005 2.792v1.404c-.596.34-1.383.51-2.362.51-1.255 0-2.255-.377-3-1.132-.744-.755-1.116-1.744-1.116-2.968 0-1.297.34-2.316 1.021-3.055.68-.74 1.548-1.11 2.6-1.11 1.033 0 1.852.323 2.458.966.606.644.91 1.572.91 2.784 0 .33-.033.676-.096 1.038h-5.314c.107.702.405 1.239.894 1.611.49.372 1.106.558 1.85.558.862 0 1.58-.202 2.155-.606zm6.605-1.77h-1.212c-.596 0-1.045.116-1.349.35-.303.234-.454.532-.454.894 0 .372.117.664.35.877.235.213.575.32 1.022.32.51 0 .912-.142 1.204-.424.293-.281.44-.651.44-1.108v-.91zm-4.068-2.554V9.325c.627-.361 1.457-.542 2.489-.542 2.116 0 3.175 1.026 3.175 3.08V17h-1.548v-.957c-.415.68-1.143 1.02-2.186 1.02-.766 0-1.38-.22-1.843-.661-.462-.442-.694-1.003-.694-1.684 0-.776.293-1.38.878-1.81.585-.431 1.404-.647 2.457-.647h1.34V11.8c0-.554-.133-.971-.399-1.253-.266-.282-.707-.423-1.324-.423a4.07 4.07 0 0 0-2.345.718zm9.333-1.93v1.42c.394-1 1.101-1.5 2.123-1.5.148 0 .313.016.494.048v1.531a1.885 1.885 0 0 0-.75-.143c-.542 0-.989.24-1.34.718-.351.479-.527 1.048-.527 1.707V17h-1.563V8.91h1.563zm5.01 4.084c.022.82.272 1.492.75 2.019.479.526 1.15.79 2.01.79.639 0 1.235-.176 1.788-.527v1.404c-.521.319-1.186.479-1.995.479-1.265 0-2.276-.4-3.031-1.197-.755-.798-1.133-1.792-1.133-2.984 0-1.16.38-2.151 1.14-2.975.761-.825 1.79-1.237 3.088-1.237.702 0 1.346.149 1.93.447v1.436a3.242 3.242 0 0 0-1.77-.495c-.84 0-1.513.266-2.019.798-.505.532-.758 1.213-.758 2.042zM40.24 5.72v4.579c.458-1 1.293-1.5 2.505-1.5.787 0 1.42.245 1.899.734.479.49.718 1.17.718 2.042V17h-1.564v-5.106c0-.553-.14-.98-.422-1.284-.282-.303-.652-.455-1.11-.455-.531 0-1.002.202-1.411.606-.41.405-.615 1.022-.615 1.851V17h-1.563V5.72h1.563zm14.966 10.02c.596 0 1.096-.253 1.5-.758.404-.506.606-1.157.606-1.955 0-.915-.202-1.62-.606-2.114-.404-.495-.92-.742-1.548-.742-.553 0-1.05.224-1.491.67-.442.447-.662 1.133-.662 2.058 0 .958.212 1.67.638 2.138.425.469.946.703 1.563.703zM53.004 5.72v4.42c.574-.894 1.388-1.341 2.44-1.341 1.022 0 1.857.383 2.506 1.149.649.766.973 1.781.973 3.047 0 1.138-.309 2.109-.925 2.912-.617.803-1.463 1.205-2.537 1.205-1.075 0-1.894-.447-2.457-1.34V17h-1.58V5.72h1.58zm9.908 11.104l-3.223-7.913h1.739l1.005 2.632 1.26 3.415c.096-.32.48-1.458 1.15-3.415l.909-2.632h1.66l-2.92 7.866c-.777 2.074-1.963 3.11-3.559 3.11a2.92 2.92 0 0 1-.734-.079v-1.34c.17.042.351.064.543.064 1.032 0 1.755-.57 2.17-1.708z"}),(0,E.jsx)("path",{fill:"#5468FF",d:"M78.988.938h16.594a2.968 2.968 0 0 1 2.966 2.966V20.5a2.967 2.967 0 0 1-2.966 2.964H78.988a2.967 2.967 0 0 1-2.966-2.964V3.897A2.961 2.961 0 0 1 78.988.938z"}),(0,E.jsx)("path",{fill:"white",d:"M89.632 5.967v-.772a.978.978 0 0 0-.978-.977h-2.28a.978.978 0 0 0-.978.977v.793c0 .088.082.15.171.13a7.127 7.127 0 0 1 1.984-.28c.65 0 1.295.088 1.917.259.082.02.164-.04.164-.13m-6.248 1.01l-.39-.389a.977.977 0 0 0-1.382 0l-.465.465a.973.973 0 0 0 0 1.38l.383.383c.062.061.15.047.205-.014.226-.307.472-.601.746-.874.281-.28.568-.526.883-.751.068-.042.075-.137.02-.2m4.16 2.453v3.341c0 .096.104.165.192.117l2.97-1.537c.068-.034.089-.117.055-.184a3.695 3.695 0 0 0-3.08-1.866c-.068 0-.136.054-.136.13m0 8.048a4.489 4.489 0 0 1-4.49-4.482 4.488 4.488 0 0 1 4.49-4.482 4.488 4.488 0 0 1 4.489 4.482 4.484 4.484 0 0 1-4.49 4.482m0-10.85a6.363 6.363 0 1 0 0 12.729 6.37 6.37 0 0 0 6.372-6.368 6.358 6.358 0 0 0-6.371-6.36"})]})})})})]}),H.items.length>0?(0,E.jsx)("main",{children:H.items.map(((e,t)=>{let{title:r,url:n,summary:i,breadcrumbs:a}=e;return(0,E.jsxs)("article",{className:_.searchResultItem,children:[(0,E.jsx)(x.Z,{as:"h2",className:_.searchResultItemHeading,children:(0,E.jsx)(l.Z,{to:n,dangerouslySetInnerHTML:{__html:r}})}),a.length>0&&(0,E.jsx)("nav",{"aria-label":"breadcrumbs",children:(0,E.jsx)("ul",{className:s("breadcrumbs",_.searchResultItemPath),children:a.map(((e,t)=>(0,E.jsx)("li",{className:"breadcrumbs__item",dangerouslySetInnerHTML:{__html:e}},t)))})}),i&&(0,E.jsx)("p",{className:_.searchResultItemSummary,dangerouslySetInnerHTML:{__html:i}})]},t)}))}):[w&&!H.loading&&(0,E.jsx)("p",{children:(0,E.jsx)(R.Z,{id:"theme.SearchPage.noResultsText",description:"The paragraph for empty search result",children:"No results were found"})},"no-results"),!!H.loading&&(0,E.jsx)("div",{className:_.loadingSpinner},"spinner")],H.hasMore&&(0,E.jsx)("div",{className:_.loader,ref:I,children:(0,E.jsx)(R.Z,{id:"theme.SearchPage.fetchingNewResults",description:"The paragraph for fetching new search results",children:"Fetching new results..."})})]})]})}function A(){return(0,E.jsx)(g.FG,{className:"search-page-wrapper",children:(0,E.jsx)(w,{})})}},8131:(e,t,r)=>{"use strict";var n=r(49374),i=r(17775),s=r(23076);function a(e,t,r){return new n(e,t,r)}a.version=r(24336),a.AlgoliaSearchHelper=n,a.SearchParameters=i,a.SearchResults=s,e.exports=a},68078:(e,t,r)=>{"use strict";var n=r(17331);function i(e,t){this.main=e,this.fn=t,this.lastResults=null}r(14853)(i,n),i.prototype.detach=function(){this.removeAllListeners(),this.main.detachDerivedHelper(this)},i.prototype.getModifiedState=function(e){return this.fn(e)},e.exports=i},82437:(e,t,r)=>{"use strict";var n=r(52344),i=r(90116),s=r(49803),a={addRefinement:function(e,t,r){if(a.isRefined(e,t,r))return e;var i=""+r,s=e[t]?e[t].concat(i):[i],c={};return c[t]=s,n({},c,e)},removeRefinement:function(e,t,r){if(void 0===r)return a.clearRefinement(e,(function(e,r){return t===r}));var n=""+r;return a.clearRefinement(e,(function(e,r){return t===r&&n===e}))},toggleRefinement:function(e,t,r){if(void 0===r)throw new Error("toggleRefinement should be used with a value");return a.isRefined(e,t,r)?a.removeRefinement(e,t,r):a.addRefinement(e,t,r)},clearRefinement:function(e,t,r){if(void 0===t)return i(e)?{}:e;if("string"==typeof t)return s(e,[t]);if("function"==typeof t){var n=!1,a=Object.keys(e).reduce((function(i,s){var a=e[s]||[],c=a.filter((function(e){return!t(e,s,r)}));return c.length!==a.length&&(n=!0),i[s]=c,i}),{});return n?a:e}},isRefined:function(e,t,r){var n=Boolean(e[t])&&e[t].length>0;if(void 0===r||!n)return n;var i=""+r;return-1!==e[t].indexOf(i)}};e.exports=a},17775:(e,t,r)=>{"use strict";var n=r(52344),i=r(7888),s=r(22686),a=r(60185),c=r(90116),u=r(49803),o=r(28023),h=r(46801),f=r(82437);function l(e,t){return Array.isArray(e)&&Array.isArray(t)?e.length===t.length&&e.every((function(e,r){return l(t[r],e)})):e===t}function m(e){var t=e?m._parseNumbers(e):{};void 0===t.userToken||h(t.userToken)||console.warn("[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}"),this.facets=t.facets||[],this.disjunctiveFacets=t.disjunctiveFacets||[],this.hierarchicalFacets=t.hierarchicalFacets||[],this.facetsRefinements=t.facetsRefinements||{},this.facetsExcludes=t.facetsExcludes||{},this.disjunctiveFacetsRefinements=t.disjunctiveFacetsRefinements||{},this.numericRefinements=t.numericRefinements||{},this.tagRefinements=t.tagRefinements||[],this.hierarchicalFacetsRefinements=t.hierarchicalFacetsRefinements||{};var r=this;Object.keys(t).forEach((function(e){var n=-1!==m.PARAMETERS.indexOf(e),i=void 0!==t[e];!n&&i&&(r[e]=t[e])}))}m.PARAMETERS=Object.keys(new m),m._parseNumbers=function(e){if(e instanceof m)return e;var t={};if(["aroundPrecision","aroundRadius","getRankingInfo","minWordSizefor2Typos","minWordSizefor1Typo","page","maxValuesPerFacet","distinct","minimumAroundRadius","hitsPerPage","minProximity"].forEach((function(r){var n=e[r];if("string"==typeof n){var i=parseFloat(n);t[r]=isNaN(i)?n:i}})),Array.isArray(e.insideBoundingBox)&&(t.insideBoundingBox=e.insideBoundingBox.map((function(e){return Array.isArray(e)?e.map((function(e){return parseFloat(e)})):e}))),e.numericRefinements){var r={};Object.keys(e.numericRefinements).forEach((function(t){var n=e.numericRefinements[t]||{};r[t]={},Object.keys(n).forEach((function(e){var i=n[e].map((function(e){return Array.isArray(e)?e.map((function(e){return"string"==typeof e?parseFloat(e):e})):"string"==typeof e?parseFloat(e):e}));r[t][e]=i}))})),t.numericRefinements=r}return a({},e,t)},m.make=function(e){var t=new m(e);return(e.hierarchicalFacets||[]).forEach((function(e){if(e.rootPath){var r=t.getHierarchicalRefinement(e.name);r.length>0&&0!==r[0].indexOf(e.rootPath)&&(t=t.clearRefinements(e.name)),0===(r=t.getHierarchicalRefinement(e.name)).length&&(t=t.toggleHierarchicalFacetRefinement(e.name,e.rootPath))}})),t},m.validate=function(e,t){var r=t||{};return e.tagFilters&&r.tagRefinements&&r.tagRefinements.length>0?new Error("[Tags] Cannot switch from the managed tag API to the advanced API. It is probably an error, if it is really what you want, you should first clear the tags with clearTags method."):e.tagRefinements.length>0&&r.tagFilters?new Error("[Tags] Cannot switch from the advanced tag API to the managed API. It is probably an error, if it is not, you should first clear the tags with clearTags method."):e.numericFilters&&r.numericRefinements&&c(r.numericRefinements)?new Error("[Numeric filters] Can't switch from the advanced to the managed API. It is probably an error, if this is really what you want, you have to first clear the numeric filters."):c(e.numericRefinements)&&r.numericFilters?new Error("[Numeric filters] Can't switch from the managed API to the advanced. It is probably an error, if this is really what you want, you have to first clear the numeric filters."):null},m.prototype={constructor:m,clearRefinements:function(e){var t={numericRefinements:this._clearNumericRefinements(e),facetsRefinements:f.clearRefinement(this.facetsRefinements,e,"conjunctiveFacet"),facetsExcludes:f.clearRefinement(this.facetsExcludes,e,"exclude"),disjunctiveFacetsRefinements:f.clearRefinement(this.disjunctiveFacetsRefinements,e,"disjunctiveFacet"),hierarchicalFacetsRefinements:f.clearRefinement(this.hierarchicalFacetsRefinements,e,"hierarchicalFacet")};return t.numericRefinements===this.numericRefinements&&t.facetsRefinements===this.facetsRefinements&&t.facetsExcludes===this.facetsExcludes&&t.disjunctiveFacetsRefinements===this.disjunctiveFacetsRefinements&&t.hierarchicalFacetsRefinements===this.hierarchicalFacetsRefinements?this:this.setQueryParameters(t)},clearTags:function(){return void 0===this.tagFilters&&0===this.tagRefinements.length?this:this.setQueryParameters({tagFilters:void 0,tagRefinements:[]})},setIndex:function(e){return e===this.index?this:this.setQueryParameters({index:e})},setQuery:function(e){return e===this.query?this:this.setQueryParameters({query:e})},setPage:function(e){return e===this.page?this:this.setQueryParameters({page:e})},setFacets:function(e){return this.setQueryParameters({facets:e})},setDisjunctiveFacets:function(e){return this.setQueryParameters({disjunctiveFacets:e})},setHitsPerPage:function(e){return this.hitsPerPage===e?this:this.setQueryParameters({hitsPerPage:e})},setTypoTolerance:function(e){return this.typoTolerance===e?this:this.setQueryParameters({typoTolerance:e})},addNumericRefinement:function(e,t,r){var n=o(r);if(this.isNumericRefined(e,t,n))return this;var i=a({},this.numericRefinements);return i[e]=a({},i[e]),i[e][t]?(i[e][t]=i[e][t].slice(),i[e][t].push(n)):i[e][t]=[n],this.setQueryParameters({numericRefinements:i})},getConjunctiveRefinements:function(e){return this.isConjunctiveFacet(e)&&this.facetsRefinements[e]||[]},getDisjunctiveRefinements:function(e){return this.isDisjunctiveFacet(e)&&this.disjunctiveFacetsRefinements[e]||[]},getHierarchicalRefinement:function(e){return this.hierarchicalFacetsRefinements[e]||[]},getExcludeRefinements:function(e){return this.isConjunctiveFacet(e)&&this.facetsExcludes[e]||[]},removeNumericRefinement:function(e,t,r){var n=r;return void 0!==n?this.isNumericRefined(e,t,n)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(r,i){return i===e&&r.op===t&&l(r.val,o(n))}))}):this:void 0!==t?this.isNumericRefined(e,t)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(r,n){return n===e&&r.op===t}))}):this:this.isNumericRefined(e)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(t,r){return r===e}))}):this},getNumericRefinements:function(e){return this.numericRefinements[e]||{}},getNumericRefinement:function(e,t){return this.numericRefinements[e]&&this.numericRefinements[e][t]},_clearNumericRefinements:function(e){if(void 0===e)return c(this.numericRefinements)?{}:this.numericRefinements;if("string"==typeof e)return u(this.numericRefinements,[e]);if("function"==typeof e){var t=!1,r=this.numericRefinements,n=Object.keys(r).reduce((function(n,i){var s=r[i],a={};return s=s||{},Object.keys(s).forEach((function(r){var n=s[r]||[],c=[];n.forEach((function(t){e({val:t,op:r},i,"numeric")||c.push(t)})),c.length!==n.length&&(t=!0),a[r]=c})),n[i]=a,n}),{});return t?n:this.numericRefinements}},addFacet:function(e){return this.isConjunctiveFacet(e)?this:this.setQueryParameters({facets:this.facets.concat([e])})},addDisjunctiveFacet:function(e){return this.isDisjunctiveFacet(e)?this:this.setQueryParameters({disjunctiveFacets:this.disjunctiveFacets.concat([e])})},addHierarchicalFacet:function(e){if(this.isHierarchicalFacet(e.name))throw new Error("Cannot declare two hierarchical facets with the same name: `"+e.name+"`");return this.setQueryParameters({hierarchicalFacets:this.hierarchicalFacets.concat([e])})},addFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsRefinements,e,t)?this:this.setQueryParameters({facetsRefinements:f.addRefinement(this.facetsRefinements,e,t)})},addExcludeRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsExcludes,e,t)?this:this.setQueryParameters({facetsExcludes:f.addRefinement(this.facetsExcludes,e,t)})},addDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return f.isRefined(this.disjunctiveFacetsRefinements,e,t)?this:this.setQueryParameters({disjunctiveFacetsRefinements:f.addRefinement(this.disjunctiveFacetsRefinements,e,t)})},addTagRefinement:function(e){if(this.isTagRefined(e))return this;var t={tagRefinements:this.tagRefinements.concat(e)};return this.setQueryParameters(t)},removeFacet:function(e){return this.isConjunctiveFacet(e)?this.clearRefinements(e).setQueryParameters({facets:this.facets.filter((function(t){return t!==e}))}):this},removeDisjunctiveFacet:function(e){return this.isDisjunctiveFacet(e)?this.clearRefinements(e).setQueryParameters({disjunctiveFacets:this.disjunctiveFacets.filter((function(t){return t!==e}))}):this},removeHierarchicalFacet:function(e){return this.isHierarchicalFacet(e)?this.clearRefinements(e).setQueryParameters({hierarchicalFacets:this.hierarchicalFacets.filter((function(t){return t.name!==e}))}):this},removeFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsRefinements,e,t)?this.setQueryParameters({facetsRefinements:f.removeRefinement(this.facetsRefinements,e,t)}):this},removeExcludeRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsExcludes,e,t)?this.setQueryParameters({facetsExcludes:f.removeRefinement(this.facetsExcludes,e,t)}):this},removeDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return f.isRefined(this.disjunctiveFacetsRefinements,e,t)?this.setQueryParameters({disjunctiveFacetsRefinements:f.removeRefinement(this.disjunctiveFacetsRefinements,e,t)}):this},removeTagRefinement:function(e){if(!this.isTagRefined(e))return this;var t={tagRefinements:this.tagRefinements.filter((function(t){return t!==e}))};return this.setQueryParameters(t)},toggleRefinement:function(e,t){return this.toggleFacetRefinement(e,t)},toggleFacetRefinement:function(e,t){if(this.isHierarchicalFacet(e))return this.toggleHierarchicalFacetRefinement(e,t);if(this.isConjunctiveFacet(e))return this.toggleConjunctiveFacetRefinement(e,t);if(this.isDisjunctiveFacet(e))return this.toggleDisjunctiveFacetRefinement(e,t);throw new Error("Cannot refine the undeclared facet "+e+"; it should be added to the helper options facets, disjunctiveFacets or hierarchicalFacets")},toggleConjunctiveFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return this.setQueryParameters({facetsRefinements:f.toggleRefinement(this.facetsRefinements,e,t)})},toggleExcludeFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return this.setQueryParameters({facetsExcludes:f.toggleRefinement(this.facetsExcludes,e,t)})},toggleDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return this.setQueryParameters({disjunctiveFacetsRefinements:f.toggleRefinement(this.disjunctiveFacetsRefinements,e,t)})},toggleHierarchicalFacetRefinement:function(e,t){if(!this.isHierarchicalFacet(e))throw new Error(e+" is not defined in the hierarchicalFacets attribute of the helper configuration");var r=this._getHierarchicalFacetSeparator(this.getHierarchicalFacetByName(e)),i={};return void 0!==this.hierarchicalFacetsRefinements[e]&&this.hierarchicalFacetsRefinements[e].length>0&&(this.hierarchicalFacetsRefinements[e][0]===t||0===this.hierarchicalFacetsRefinements[e][0].indexOf(t+r))?-1===t.indexOf(r)?i[e]=[]:i[e]=[t.slice(0,t.lastIndexOf(r))]:i[e]=[t],this.setQueryParameters({hierarchicalFacetsRefinements:n({},i,this.hierarchicalFacetsRefinements)})},addHierarchicalFacetRefinement:function(e,t){if(this.isHierarchicalFacetRefined(e))throw new Error(e+" is already refined.");if(!this.isHierarchicalFacet(e))throw new Error(e+" is not defined in the hierarchicalFacets attribute of the helper configuration.");var r={};return r[e]=[t],this.setQueryParameters({hierarchicalFacetsRefinements:n({},r,this.hierarchicalFacetsRefinements)})},removeHierarchicalFacetRefinement:function(e){if(!this.isHierarchicalFacetRefined(e))return this;var t={};return t[e]=[],this.setQueryParameters({hierarchicalFacetsRefinements:n({},t,this.hierarchicalFacetsRefinements)})},toggleTagRefinement:function(e){return this.isTagRefined(e)?this.removeTagRefinement(e):this.addTagRefinement(e)},isDisjunctiveFacet:function(e){return this.disjunctiveFacets.indexOf(e)>-1},isHierarchicalFacet:function(e){return void 0!==this.getHierarchicalFacetByName(e)},isConjunctiveFacet:function(e){return this.facets.indexOf(e)>-1},isFacetRefined:function(e,t){return!!this.isConjunctiveFacet(e)&&f.isRefined(this.facetsRefinements,e,t)},isExcludeRefined:function(e,t){return!!this.isConjunctiveFacet(e)&&f.isRefined(this.facetsExcludes,e,t)},isDisjunctiveFacetRefined:function(e,t){return!!this.isDisjunctiveFacet(e)&&f.isRefined(this.disjunctiveFacetsRefinements,e,t)},isHierarchicalFacetRefined:function(e,t){if(!this.isHierarchicalFacet(e))return!1;var r=this.getHierarchicalRefinement(e);return t?-1!==r.indexOf(t):r.length>0},isNumericRefined:function(e,t,r){if(void 0===r&&void 0===t)return Boolean(this.numericRefinements[e]);var n=this.numericRefinements[e]&&void 0!==this.numericRefinements[e][t];if(void 0===r||!n)return n;var s,a,c=o(r),u=void 0!==(s=this.numericRefinements[e][t],a=c,i(s,(function(e){return l(e,a)})));return n&&u},isTagRefined:function(e){return-1!==this.tagRefinements.indexOf(e)},getRefinedDisjunctiveFacets:function(){var e=this,t=s(Object.keys(this.numericRefinements).filter((function(t){return Object.keys(e.numericRefinements[t]).length>0})),this.disjunctiveFacets);return Object.keys(this.disjunctiveFacetsRefinements).filter((function(t){return e.disjunctiveFacetsRefinements[t].length>0})).concat(t).concat(this.getRefinedHierarchicalFacets()).sort()},getRefinedHierarchicalFacets:function(){var e=this;return s(this.hierarchicalFacets.map((function(e){return e.name})),Object.keys(this.hierarchicalFacetsRefinements).filter((function(t){return e.hierarchicalFacetsRefinements[t].length>0}))).sort()},getUnrefinedDisjunctiveFacets:function(){var e=this.getRefinedDisjunctiveFacets();return this.disjunctiveFacets.filter((function(t){return-1===e.indexOf(t)}))},managedParameters:["index","facets","disjunctiveFacets","facetsRefinements","hierarchicalFacets","facetsExcludes","disjunctiveFacetsRefinements","numericRefinements","tagRefinements","hierarchicalFacetsRefinements"],getQueryParams:function(){var e=this.managedParameters,t={},r=this;return Object.keys(this).forEach((function(n){var i=r[n];-1===e.indexOf(n)&&void 0!==i&&(t[n]=i)})),t},setQueryParameter:function(e,t){if(this[e]===t)return this;var r={};return r[e]=t,this.setQueryParameters(r)},setQueryParameters:function(e){if(!e)return this;var t=m.validate(this,e);if(t)throw t;var r=this,n=m._parseNumbers(e),i=Object.keys(this).reduce((function(e,t){return e[t]=r[t],e}),{}),s=Object.keys(n).reduce((function(e,t){var r=void 0!==e[t],i=void 0!==n[t];return r&&!i?u(e,[t]):(i&&(e[t]=n[t]),e)}),i);return new this.constructor(s)},resetPage:function(){return void 0===this.page?this:this.setPage(0)},_getHierarchicalFacetSortBy:function(e){return e.sortBy||["isRefined:desc","name:asc"]},_getHierarchicalFacetSeparator:function(e){return e.separator||" > "},_getHierarchicalRootPath:function(e){return e.rootPath||null},_getHierarchicalShowParentLevel:function(e){return"boolean"!=typeof e.showParentLevel||e.showParentLevel},getHierarchicalFacetByName:function(e){return i(this.hierarchicalFacets,(function(t){return t.name===e}))},getHierarchicalFacetBreadcrumb:function(e){if(!this.isHierarchicalFacet(e))return[];var t=this.getHierarchicalRefinement(e)[0];if(!t)return[];var r=this._getHierarchicalFacetSeparator(this.getHierarchicalFacetByName(e));return t.split(r).map((function(e){return e.trim()}))},toString:function(){return JSON.stringify(this,null,2)}},e.exports=m},10210:(e,t,r)=>{"use strict";e.exports=function(e){return function(t,r){var n=e.hierarchicalFacets[r],o=e.hierarchicalFacetsRefinements[n.name]&&e.hierarchicalFacetsRefinements[n.name][0]||"",h=e._getHierarchicalFacetSeparator(n),f=e._getHierarchicalRootPath(n),l=e._getHierarchicalShowParentLevel(n),m=s(e._getHierarchicalFacetSortBy(n)),d=t.every((function(e){return e.exhaustive})),p=function(e,t,r,n,s){return function(o,h,f){var l=o;if(f>0){var m=0;for(l=o;m<f;){var d=l&&Array.isArray(l.data)?l.data:[];l=i(d,(function(e){return e.isRefined})),m++}}if(l){var p=Object.keys(h.data).map((function(e){return[e,h.data[e]]})).filter((function(e){return function(e,t,r,n,i,s){if(i&&(0!==e.indexOf(i)||i===e))return!1;return!i&&-1===e.indexOf(n)||i&&e.split(n).length-i.split(n).length==1||-1===e.indexOf(n)&&-1===r.indexOf(n)||0===r.indexOf(e)||0===e.indexOf(t+n)&&(s||0===e.indexOf(r))}(e[0],l.path||r,s,t,r,n)}));l.data=a(p.map((function(e){var r=e[0];return function(e,t,r,n,i){var s=t.split(r);return{name:s[s.length-1].trim(),path:t,escapedValue:c(t),count:e,isRefined:n===t||0===n.indexOf(t+r),exhaustive:i,data:null}}(e[1],r,t,u(s),h.exhaustive)})),e[0],e[1])}return o}}(m,h,f,l,o),v=t;return f&&(v=t.slice(f.split(h).length)),v.reduce(p,{name:e.hierarchicalFacets[r].name,count:null,isRefined:!0,path:null,escapedValue:null,exhaustive:d,data:null})}};var n=r(94039),i=r(7888),s=r(82293),a=r(42148),c=n.escapeFacetValue,u=n.unescapeFacetValue},23076:(e,t,r)=>{"use strict";var n=r(74587),i=r(52344),s=r(94039),a=r(7888),c=r(69725),u=r(82293),o=r(60185),h=r(42148),f=s.escapeFacetValue,l=s.unescapeFacetValue,m=r(10210);function d(e){var t={};return e.forEach((function(e,r){t[e]=r})),t}function p(e,t,r){t&&t[r]&&(e.stats=t[r])}function v(e,t,r){var s=t[0];this._rawResults=t;var u=this;Object.keys(s).forEach((function(e){u[e]=s[e]})),Object.keys(r||{}).forEach((function(e){u[e]=r[e]})),this.processingTimeMS=t.reduce((function(e,t){return void 0===t.processingTimeMS?e:e+t.processingTimeMS}),0),this.disjunctiveFacets=[],this.hierarchicalFacets=e.hierarchicalFacets.map((function(){return[]})),this.facets=[];var h=e.getRefinedDisjunctiveFacets(),f=d(e.facets),v=d(e.disjunctiveFacets),g=1,y=s.facets||{};Object.keys(y).forEach((function(t){var r,n,i=y[t],o=(r=e.hierarchicalFacets,n=t,a(r,(function(e){return(e.attributes||[]).indexOf(n)>-1})));if(o){var h=o.attributes.indexOf(t),l=c(e.hierarchicalFacets,(function(e){return e.name===o.name}));u.hierarchicalFacets[l][h]={attribute:t,data:i,exhaustive:s.exhaustiveFacetsCount}}else{var m,d=-1!==e.disjunctiveFacets.indexOf(t),g=-1!==e.facets.indexOf(t);d&&(m=v[t],u.disjunctiveFacets[m]={name:t,data:i,exhaustive:s.exhaustiveFacetsCount},p(u.disjunctiveFacets[m],s.facets_stats,t)),g&&(m=f[t],u.facets[m]={name:t,data:i,exhaustive:s.exhaustiveFacetsCount},p(u.facets[m],s.facets_stats,t))}})),this.hierarchicalFacets=n(this.hierarchicalFacets),h.forEach((function(r){var n=t[g],a=n&&n.facets?n.facets:{},h=e.getHierarchicalFacetByName(r);Object.keys(a).forEach((function(t){var r,f=a[t];if(h){r=c(e.hierarchicalFacets,(function(e){return e.name===h.name}));var m=c(u.hierarchicalFacets[r],(function(e){return e.attribute===t}));if(-1===m)return;u.hierarchicalFacets[r][m].data=o({},u.hierarchicalFacets[r][m].data,f)}else{r=v[t];var d=s.facets&&s.facets[t]||{};u.disjunctiveFacets[r]={name:t,data:i({},f,d),exhaustive:n.exhaustiveFacetsCount},p(u.disjunctiveFacets[r],n.facets_stats,t),e.disjunctiveFacetsRefinements[t]&&e.disjunctiveFacetsRefinements[t].forEach((function(n){!u.disjunctiveFacets[r].data[n]&&e.disjunctiveFacetsRefinements[t].indexOf(l(n))>-1&&(u.disjunctiveFacets[r].data[n]=0)}))}})),g++})),e.getRefinedHierarchicalFacets().forEach((function(r){var n=e.getHierarchicalFacetByName(r),s=e._getHierarchicalFacetSeparator(n),a=e.getHierarchicalRefinement(r);0===a.length||a[0].split(s).length<2||t.slice(g).forEach((function(t){var r=t&&t.facets?t.facets:{};Object.keys(r).forEach((function(t){var o=r[t],h=c(e.hierarchicalFacets,(function(e){return e.name===n.name})),f=c(u.hierarchicalFacets[h],(function(e){return e.attribute===t}));if(-1!==f){var l={};if(a.length>0){var m=a[0].split(s)[0];l[m]=u.hierarchicalFacets[h][f].data[m]}u.hierarchicalFacets[h][f].data=i(l,o,u.hierarchicalFacets[h][f].data)}})),g++}))})),Object.keys(e.facetsExcludes).forEach((function(t){var r=e.facetsExcludes[t],n=f[t];u.facets[n]={name:t,data:y[t],exhaustive:s.exhaustiveFacetsCount},r.forEach((function(e){u.facets[n]=u.facets[n]||{name:t},u.facets[n].data=u.facets[n].data||{},u.facets[n].data[e]=0}))})),this.hierarchicalFacets=this.hierarchicalFacets.map(m(e)),this.facets=n(this.facets),this.disjunctiveFacets=n(this.disjunctiveFacets),this._state=e}function g(e,t){function r(e){return e.name===t}if(e._state.isConjunctiveFacet(t)){var n=a(e.facets,r);return n?Object.keys(n.data).map((function(r){var i=f(r);return{name:r,escapedValue:i,count:n.data[r],isRefined:e._state.isFacetRefined(t,i),isExcluded:e._state.isExcludeRefined(t,r)}})):[]}if(e._state.isDisjunctiveFacet(t)){var i=a(e.disjunctiveFacets,r);return i?Object.keys(i.data).map((function(r){var n=f(r);return{name:r,escapedValue:n,count:i.data[r],isRefined:e._state.isDisjunctiveFacetRefined(t,n)}})):[]}if(e._state.isHierarchicalFacet(t)){var s=a(e.hierarchicalFacets,r);if(!s)return s;var c=e._state.getHierarchicalFacetByName(t),u=e._state._getHierarchicalFacetSeparator(c),o=l(e._state.getHierarchicalRefinement(t)[0]||"");0===o.indexOf(c.rootPath)&&(o=o.replace(c.rootPath+u,""));var h=o.split(u);return h.unshift(t),y(s,h,0),s}}function y(e,t,r){e.isRefined=e.name===t[r],e.data&&e.data.forEach((function(e){y(e,t,r+1)}))}function R(e,t,r,n){if(n=n||0,Array.isArray(t))return e(t,r[n]);if(!t.data||0===t.data.length)return t;var s=t.data.map((function(t){return R(e,t,r,n+1)})),a=e(s,r[n]);return i({data:a},t)}function F(e,t){var r=a(e,(function(e){return e.name===t}));return r&&r.stats}function b(e,t,r,n,i){var s=a(i,(function(e){return e.name===r})),c=s&&s.data&&s.data[n]?s.data[n]:0,u=s&&s.exhaustive||!1;return{type:t,attributeName:r,name:n,count:c,exhaustive:u}}v.prototype.getFacetByName=function(e){function t(t){return t.name===e}return a(this.facets,t)||a(this.disjunctiveFacets,t)||a(this.hierarchicalFacets,t)},v.DEFAULT_SORT=["isRefined:desc","count:desc","name:asc"],v.prototype.getFacetValues=function(e,t){var r=g(this,e);if(r){var n,s=i({},t,{sortBy:v.DEFAULT_SORT,facetOrdering:!(t&&t.sortBy)}),a=this;if(Array.isArray(r))n=[e];else n=a._state.getHierarchicalFacetByName(r.name).attributes;return R((function(e,t){if(s.facetOrdering){var r=function(e,t){return e.renderingContent&&e.renderingContent.facetOrdering&&e.renderingContent.facetOrdering.values&&e.renderingContent.facetOrdering.values[t]}(a,t);if(r)return function(e,t){var r=[],n=[],i=(t.order||[]).reduce((function(e,t,r){return e[t]=r,e}),{});e.forEach((function(e){var t=e.path||e.name;void 0!==i[t]?r[i[t]]=e:n.push(e)})),r=r.filter((function(e){return e}));var s,a=t.sortRemainingBy;return"hidden"===a?r:(s="alpha"===a?[["path","name"],["asc","asc"]]:[["count"],["desc"]],r.concat(h(n,s[0],s[1])))}(e,r)}if(Array.isArray(s.sortBy)){var n=u(s.sortBy,v.DEFAULT_SORT);return h(e,n[0],n[1])}if("function"==typeof s.sortBy)return function(e,t){return t.sort(e)}(s.sortBy,e);throw new Error("options.sortBy is optional but if defined it must be either an array of string (predicates) or a sorting function")}),r,n)}},v.prototype.getFacetStats=function(e){return this._state.isConjunctiveFacet(e)?F(this.facets,e):this._state.isDisjunctiveFacet(e)?F(this.disjunctiveFacets,e):void 0},v.prototype.getRefinements=function(){var e=this._state,t=this,r=[];return Object.keys(e.facetsRefinements).forEach((function(n){e.facetsRefinements[n].forEach((function(i){r.push(b(e,"facet",n,i,t.facets))}))})),Object.keys(e.facetsExcludes).forEach((function(n){e.facetsExcludes[n].forEach((function(i){r.push(b(e,"exclude",n,i,t.facets))}))})),Object.keys(e.disjunctiveFacetsRefinements).forEach((function(n){e.disjunctiveFacetsRefinements[n].forEach((function(i){r.push(b(e,"disjunctive",n,i,t.disjunctiveFacets))}))})),Object.keys(e.hierarchicalFacetsRefinements).forEach((function(n){e.hierarchicalFacetsRefinements[n].forEach((function(i){r.push(function(e,t,r,n){var i=e.getHierarchicalFacetByName(t),s=e._getHierarchicalFacetSeparator(i),c=r.split(s),u=a(n,(function(e){return e.name===t})),o=c.reduce((function(e,t){var r=e&&a(e.data,(function(e){return e.name===t}));return void 0!==r?r:e}),u),h=o&&o.count||0,f=o&&o.exhaustive||!1,l=o&&o.path||"";return{type:"hierarchical",attributeName:t,name:l,count:h,exhaustive:f}}(e,n,i,t.hierarchicalFacets))}))})),Object.keys(e.numericRefinements).forEach((function(t){var n=e.numericRefinements[t];Object.keys(n).forEach((function(e){n[e].forEach((function(n){r.push({type:"numeric",attributeName:t,name:n,numericValue:n,operator:e})}))}))})),e.tagRefinements.forEach((function(e){r.push({type:"tag",attributeName:"_tags",name:e})})),r},e.exports=v},49374:(e,t,r)=>{"use strict";var n=r(17331),i=r(68078),s=r(94039).escapeFacetValue,a=r(14853),c=r(60185),u=r(90116),o=r(49803),h=r(96394),f=r(17775),l=r(23076),m=r(24336);function d(e,t,r){"function"==typeof e.addAlgoliaAgent&&e.addAlgoliaAgent("JS Helper ("+m+")"),this.setClient(e);var n=r||{};n.index=t,this.state=f.make(n),this.lastResults=null,this._queryId=0,this._lastQueryIdReceived=-1,this.derivedHelpers=[],this._currentNbQueries=0}function p(e){if(e<0)throw new Error("Page requested below 0.");return this._change({state:this.state.setPage(e),isPageReset:!1}),this}function v(){return this.state.page}a(d,n),d.prototype.search=function(){return this._search({onlyWithDerivedHelpers:!1}),this},d.prototype.searchOnlyWithDerivedHelpers=function(){return this._search({onlyWithDerivedHelpers:!0}),this},d.prototype.getQuery=function(){var e=this.state;return h._getHitsSearchParams(e)},d.prototype.searchOnce=function(e,t){var r=e?this.state.setQueryParameters(e):this.state,n=h._getQueries(r.index,r),i=this;if(this._currentNbQueries++,this.emit("searchOnce",{state:r}),!t)return this.client.search(n).then((function(e){return i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),{content:new l(r,e.results),state:r,_originalResponse:e}}),(function(e){throw i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),e}));this.client.search(n).then((function(e){i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),t(null,new l(r,e.results),r)})).catch((function(e){i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),t(e,null,r)}))},d.prototype.findAnswers=function(e){console.warn("[algoliasearch-helper] answers is no longer supported");var t=this.state,r=this.derivedHelpers[0];if(!r)return Promise.resolve([]);var n=r.getModifiedState(t),i=c({attributesForPrediction:e.attributesForPrediction,nbHits:e.nbHits},{params:o(h._getHitsSearchParams(n),["attributesToSnippet","hitsPerPage","restrictSearchableAttributes","snippetEllipsisText"])}),s="search for answers was called, but this client does not have a function client.initIndex(index).findAnswers";if("function"!=typeof this.client.initIndex)throw new Error(s);var a=this.client.initIndex(n.index);if("function"!=typeof a.findAnswers)throw new Error(s);return a.findAnswers(n.query,e.queryLanguages,i)},d.prototype.searchForFacetValues=function(e,t,r,n){var i="function"==typeof this.client.searchForFacetValues,a="function"==typeof this.client.initIndex;if(!i&&!a&&"function"!=typeof this.client.search)throw new Error("search for facet values (searchable) was called, but this client does not have a function client.searchForFacetValues or client.initIndex(index).searchForFacetValues");var c=this.state.setQueryParameters(n||{}),u=c.isDisjunctiveFacet(e),o=h.getSearchForFacetQuery(e,t,r,c);this._currentNbQueries++;var f,l=this;return i?f=this.client.searchForFacetValues([{indexName:c.index,params:o}]):a?f=this.client.initIndex(c.index).searchForFacetValues(o):(delete o.facetName,f=this.client.search([{type:"facet",facet:e,indexName:c.index,params:o}]).then((function(e){return e.results[0]}))),this.emit("searchForFacetValues",{state:c,facet:e,query:t}),f.then((function(t){return l._currentNbQueries--,0===l._currentNbQueries&&l.emit("searchQueueEmpty"),(t=Array.isArray(t)?t[0]:t).facetHits.forEach((function(t){t.escapedValue=s(t.value),t.isRefined=u?c.isDisjunctiveFacetRefined(e,t.escapedValue):c.isFacetRefined(e,t.escapedValue)})),t}),(function(e){throw l._currentNbQueries--,0===l._currentNbQueries&&l.emit("searchQueueEmpty"),e}))},d.prototype.setQuery=function(e){return this._change({state:this.state.resetPage().setQuery(e),isPageReset:!0}),this},d.prototype.clearRefinements=function(e){return this._change({state:this.state.resetPage().clearRefinements(e),isPageReset:!0}),this},d.prototype.clearTags=function(){return this._change({state:this.state.resetPage().clearTags(),isPageReset:!0}),this},d.prototype.addDisjunctiveFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addDisjunctiveFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addDisjunctiveRefine=function(){return this.addDisjunctiveFacetRefinement.apply(this,arguments)},d.prototype.addHierarchicalFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addHierarchicalFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addNumericRefinement=function(e,t,r){return this._change({state:this.state.resetPage().addNumericRefinement(e,t,r),isPageReset:!0}),this},d.prototype.addFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addRefine=function(){return this.addFacetRefinement.apply(this,arguments)},d.prototype.addFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().addExcludeRefinement(e,t),isPageReset:!0}),this},d.prototype.addExclude=function(){return this.addFacetExclusion.apply(this,arguments)},d.prototype.addTag=function(e){return this._change({state:this.state.resetPage().addTagRefinement(e),isPageReset:!0}),this},d.prototype.removeNumericRefinement=function(e,t,r){return this._change({state:this.state.resetPage().removeNumericRefinement(e,t,r),isPageReset:!0}),this},d.prototype.removeDisjunctiveFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().removeDisjunctiveFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.removeDisjunctiveRefine=function(){return this.removeDisjunctiveFacetRefinement.apply(this,arguments)},d.prototype.removeHierarchicalFacetRefinement=function(e){return this._change({state:this.state.resetPage().removeHierarchicalFacetRefinement(e),isPageReset:!0}),this},d.prototype.removeFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().removeFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.removeRefine=function(){return this.removeFacetRefinement.apply(this,arguments)},d.prototype.removeFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().removeExcludeRefinement(e,t),isPageReset:!0}),this},d.prototype.removeExclude=function(){return this.removeFacetExclusion.apply(this,arguments)},d.prototype.removeTag=function(e){return this._change({state:this.state.resetPage().removeTagRefinement(e),isPageReset:!0}),this},d.prototype.toggleFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().toggleExcludeFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.toggleExclude=function(){return this.toggleFacetExclusion.apply(this,arguments)},d.prototype.toggleRefinement=function(e,t){return this.toggleFacetRefinement(e,t)},d.prototype.toggleFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().toggleFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.toggleRefine=function(){return this.toggleFacetRefinement.apply(this,arguments)},d.prototype.toggleTag=function(e){return this._change({state:this.state.resetPage().toggleTagRefinement(e),isPageReset:!0}),this},d.prototype.nextPage=function(){var e=this.state.page||0;return this.setPage(e+1)},d.prototype.previousPage=function(){var e=this.state.page||0;return this.setPage(e-1)},d.prototype.setCurrentPage=p,d.prototype.setPage=p,d.prototype.setIndex=function(e){return this._change({state:this.state.resetPage().setIndex(e),isPageReset:!0}),this},d.prototype.setQueryParameter=function(e,t){return this._change({state:this.state.resetPage().setQueryParameter(e,t),isPageReset:!0}),this},d.prototype.setState=function(e){return this._change({state:f.make(e),isPageReset:!1}),this},d.prototype.overrideStateWithoutTriggeringChangeEvent=function(e){return this.state=new f(e),this},d.prototype.hasRefinements=function(e){return!!u(this.state.getNumericRefinements(e))||(this.state.isConjunctiveFacet(e)?this.state.isFacetRefined(e):this.state.isDisjunctiveFacet(e)?this.state.isDisjunctiveFacetRefined(e):!!this.state.isHierarchicalFacet(e)&&this.state.isHierarchicalFacetRefined(e))},d.prototype.isExcluded=function(e,t){return this.state.isExcludeRefined(e,t)},d.prototype.isDisjunctiveRefined=function(e,t){return this.state.isDisjunctiveFacetRefined(e,t)},d.prototype.hasTag=function(e){return this.state.isTagRefined(e)},d.prototype.isTagRefined=function(){return this.hasTagRefinements.apply(this,arguments)},d.prototype.getIndex=function(){return this.state.index},d.prototype.getCurrentPage=v,d.prototype.getPage=v,d.prototype.getTags=function(){return this.state.tagRefinements},d.prototype.getRefinements=function(e){var t=[];if(this.state.isConjunctiveFacet(e))this.state.getConjunctiveRefinements(e).forEach((function(e){t.push({value:e,type:"conjunctive"})})),this.state.getExcludeRefinements(e).forEach((function(e){t.push({value:e,type:"exclude"})}));else if(this.state.isDisjunctiveFacet(e)){this.state.getDisjunctiveRefinements(e).forEach((function(e){t.push({value:e,type:"disjunctive"})}))}var r=this.state.getNumericRefinements(e);return Object.keys(r).forEach((function(e){var n=r[e];t.push({value:n,operator:e,type:"numeric"})})),t},d.prototype.getNumericRefinement=function(e,t){return this.state.getNumericRefinement(e,t)},d.prototype.getHierarchicalFacetBreadcrumb=function(e){return this.state.getHierarchicalFacetBreadcrumb(e)},d.prototype._search=function(e){var t=this.state,r=[],n=[];e.onlyWithDerivedHelpers||(n=h._getQueries(t.index,t),r.push({state:t,queriesCount:n.length,helper:this}),this.emit("search",{state:t,results:this.lastResults}));var i=this.derivedHelpers.map((function(e){var n=e.getModifiedState(t),i=n.index?h._getQueries(n.index,n):[];return r.push({state:n,queriesCount:i.length,helper:e}),e.emit("search",{state:n,results:e.lastResults}),i})),s=Array.prototype.concat.apply(n,i),a=this._queryId++;if(this._currentNbQueries++,!s.length)return Promise.resolve({results:[]}).then(this._dispatchAlgoliaResponse.bind(this,r,a));try{this.client.search(s).then(this._dispatchAlgoliaResponse.bind(this,r,a)).catch(this._dispatchAlgoliaError.bind(this,a))}catch(c){this.emit("error",{error:c})}},d.prototype._dispatchAlgoliaResponse=function(e,t,r){if(!(t<this._lastQueryIdReceived)){this._currentNbQueries-=t-this._lastQueryIdReceived,this._lastQueryIdReceived=t,0===this._currentNbQueries&&this.emit("searchQueueEmpty");var n=r.results.slice();e.forEach((function(e){var t=e.state,r=e.queriesCount,i=e.helper,s=n.splice(0,r);t.index?(i.lastResults=new l(t,s),i.emit("result",{results:i.lastResults,state:t})):i.emit("result",{results:null,state:t})}))}},d.prototype._dispatchAlgoliaError=function(e,t){e<this._lastQueryIdReceived||(this._currentNbQueries-=e-this._lastQueryIdReceived,this._lastQueryIdReceived=e,this.emit("error",{error:t}),0===this._currentNbQueries&&this.emit("searchQueueEmpty"))},d.prototype.containsRefinement=function(e,t,r,n){return e||0!==t.length||0!==r.length||0!==n.length},d.prototype._hasDisjunctiveRefinements=function(e){return this.state.disjunctiveRefinements[e]&&this.state.disjunctiveRefinements[e].length>0},d.prototype._change=function(e){var t=e.state,r=e.isPageReset;t!==this.state&&(this.state=t,this.emit("change",{state:this.state,results:this.lastResults,isPageReset:r}))},d.prototype.clearCache=function(){return this.client.clearCache&&this.client.clearCache(),this},d.prototype.setClient=function(e){return this.client===e||("function"==typeof e.addAlgoliaAgent&&e.addAlgoliaAgent("JS Helper ("+m+")"),this.client=e),this},d.prototype.getClient=function(){return this.client},d.prototype.derive=function(e){var t=new i(this,e);return this.derivedHelpers.push(t),t},d.prototype.detachDerivedHelper=function(e){var t=this.derivedHelpers.indexOf(e);if(-1===t)throw new Error("Derived helper already detached");this.derivedHelpers.splice(t,1)},d.prototype.hasPendingRequests=function(){return this._currentNbQueries>0},e.exports=d},74587:e=>{"use strict";e.exports=function(e){return Array.isArray(e)?e.filter(Boolean):[]}},52344:e=>{"use strict";e.exports=function(){return Array.prototype.slice.call(arguments).reduceRight((function(e,t){return Object.keys(Object(t)).forEach((function(r){void 0!==t[r]&&(void 0!==e[r]&&delete e[r],e[r]=t[r])})),e}),{})}},94039:e=>{"use strict";e.exports={escapeFacetValue:function(e){return"string"!=typeof e?e:String(e).replace(/^-/,"\\-")},unescapeFacetValue:function(e){return"string"!=typeof e?e:e.replace(/^\\-/,"-")}}},7888:e=>{"use strict";e.exports=function(e,t){if(Array.isArray(e))for(var r=0;r<e.length;r++)if(t(e[r]))return e[r]}},69725:e=>{"use strict";e.exports=function(e,t){if(!Array.isArray(e))return-1;for(var r=0;r<e.length;r++)if(t(e[r]))return r;return-1}},82293:(e,t,r)=>{"use strict";var n=r(7888);e.exports=function(e,t){var r=(t||[]).map((function(e){return e.split(":")}));return e.reduce((function(e,t){var i=t.split(":"),s=n(r,(function(e){return e[0]===i[0]}));return i.length>1||!s?(e[0].push(i[0]),e[1].push(i[1]),e):(e[0].push(s[0]),e[1].push(s[1]),e)}),[[],[]])}},14853:e=>{"use strict";e.exports=function(e,t){e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}},22686:e=>{"use strict";e.exports=function(e,t){return e.filter((function(r,n){return t.indexOf(r)>-1&&e.indexOf(r)===n}))}},60185:e=>{"use strict";function t(e){return"function"==typeof e||Array.isArray(e)||"[object Object]"===Object.prototype.toString.call(e)}function r(e,n){if(e===n)return e;for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)&&"__proto__"!==i&&"constructor"!==i){var s=n[i],a=e[i];void 0!==a&&void 0===s||(t(a)&&t(s)?e[i]=r(a,s):e[i]="object"==typeof(c=s)&&null!==c?r(Array.isArray(c)?[]:{},c):c)}var c;return e}e.exports=function(e){t(e)||(e={});for(var n=1,i=arguments.length;n<i;n++){var s=arguments[n];t(s)&&r(e,s)}return e}},90116:e=>{"use strict";e.exports=function(e){return e&&Object.keys(e).length>0}},49803:e=>{"use strict";e.exports=function(e,t){if(null===e)return{};var r,n,i={},s=Object.keys(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||(i[r]=e[r]);return i}},42148:e=>{"use strict";function t(e,t){if(e!==t){var r=void 0!==e,n=null===e,i=void 0!==t,s=null===t;if(!s&&e>t||n&&i||!r)return 1;if(!n&&e<t||s&&r||!i)return-1}return 0}e.exports=function(e,r,n){if(!Array.isArray(e))return[];Array.isArray(n)||(n=[]);var i=e.map((function(e,t){return{criteria:r.map((function(t){return e[t]})),index:t,value:e}}));return i.sort((function(e,r){for(var i=-1;++i<e.criteria.length;){var s=t(e.criteria[i],r.criteria[i]);if(s)return i>=n.length?s:"desc"===n[i]?-s:s}return e.index-r.index})),i.map((function(e){return e.value}))}},28023:e=>{"use strict";e.exports=function e(t){if("number"==typeof t)return t;if("string"==typeof t)return parseFloat(t);if(Array.isArray(t))return t.map(e);throw new Error("The value should be a number, a parsable string or an array of those.")}},96394:(e,t,r)=>{"use strict";var n=r(60185);function i(e){return Object.keys(e).sort().reduce((function(t,r){return t[r]=e[r],t}),{})}var s={_getQueries:function(e,t){var r=[];return r.push({indexName:e,params:s._getHitsSearchParams(t)}),t.getRefinedDisjunctiveFacets().forEach((function(n){r.push({indexName:e,params:s._getDisjunctiveFacetSearchParams(t,n)})})),t.getRefinedHierarchicalFacets().forEach((function(n){var i=t.getHierarchicalFacetByName(n),a=t.getHierarchicalRefinement(n),c=t._getHierarchicalFacetSeparator(i);if(a.length>0&&a[0].split(c).length>1){var u=a[0].split(c).slice(0,-1).reduce((function(e,t,r){return e.concat({attribute:i.attributes[r],value:0===r?t:[e[e.length-1].value,t].join(c)})}),[]);u.forEach((function(n,a){var c=s._getDisjunctiveFacetSearchParams(t,n.attribute,0===a);function o(e){return i.attributes.some((function(t){return t===e.split(":")[0]}))}var h=(c.facetFilters||[]).reduce((function(e,t){if(Array.isArray(t)){var r=t.filter((function(e){return!o(e)}));r.length>0&&e.push(r)}return"string"!=typeof t||o(t)||e.push(t),e}),[]),f=u[a-1];c.facetFilters=a>0?h.concat(f.attribute+":"+f.value):h.length>0?h:void 0,r.push({indexName:e,params:c})}))}})),r},_getHitsSearchParams:function(e){var t=e.facets.concat(e.disjunctiveFacets).concat(s._getHitsHierarchicalFacetsAttributes(e)).sort(),r=s._getFacetFilters(e),a=s._getNumericFilters(e),c=s._getTagFilters(e),u={facets:t.indexOf("*")>-1?["*"]:t,tagFilters:c};return r.length>0&&(u.facetFilters=r),a.length>0&&(u.numericFilters=a),i(n({},e.getQueryParams(),u))},_getDisjunctiveFacetSearchParams:function(e,t,r){var a=s._getFacetFilters(e,t,r),c=s._getNumericFilters(e,t),u=s._getTagFilters(e),o={hitsPerPage:0,page:0,analytics:!1,clickAnalytics:!1};u.length>0&&(o.tagFilters=u);var h=e.getHierarchicalFacetByName(t);return o.facets=h?s._getDisjunctiveHierarchicalFacetAttribute(e,h,r):t,c.length>0&&(o.numericFilters=c),a.length>0&&(o.facetFilters=a),i(n({},e.getQueryParams(),o))},_getNumericFilters:function(e,t){if(e.numericFilters)return e.numericFilters;var r=[];return Object.keys(e.numericRefinements).forEach((function(n){var i=e.numericRefinements[n]||{};Object.keys(i).forEach((function(e){var s=i[e]||[];t!==n&&s.forEach((function(t){if(Array.isArray(t)){var i=t.map((function(t){return n+e+t}));r.push(i)}else r.push(n+e+t)}))}))})),r},_getTagFilters:function(e){return e.tagFilters?e.tagFilters:e.tagRefinements.join(",")},_getFacetFilters:function(e,t,r){var n=[],i=e.facetsRefinements||{};Object.keys(i).sort().forEach((function(e){(i[e]||[]).sort().forEach((function(t){n.push(e+":"+t)}))}));var s=e.facetsExcludes||{};Object.keys(s).sort().forEach((function(e){(s[e]||[]).sort().forEach((function(t){n.push(e+":-"+t)}))}));var a=e.disjunctiveFacetsRefinements||{};Object.keys(a).sort().forEach((function(e){var r=a[e]||[];if(e!==t&&r&&0!==r.length){var i=[];r.sort().forEach((function(t){i.push(e+":"+t)})),n.push(i)}}));var c=e.hierarchicalFacetsRefinements||{};return Object.keys(c).sort().forEach((function(i){var s=(c[i]||[])[0];if(void 0!==s){var a,u,o=e.getHierarchicalFacetByName(i),h=e._getHierarchicalFacetSeparator(o),f=e._getHierarchicalRootPath(o);if(t===i){if(-1===s.indexOf(h)||!f&&!0===r||f&&f.split(h).length===s.split(h).length)return;f?(u=f.split(h).length-1,s=f):(u=s.split(h).length-2,s=s.slice(0,s.lastIndexOf(h))),a=o.attributes[u]}else u=s.split(h).length-1,a=o.attributes[u];a&&n.push([a+":"+s])}})),n},_getHitsHierarchicalFacetsAttributes:function(e){return e.hierarchicalFacets.reduce((function(t,r){var n=e.getHierarchicalRefinement(r.name)[0];if(!n)return t.push(r.attributes[0]),t;var i=e._getHierarchicalFacetSeparator(r),s=n.split(i).length,a=r.attributes.slice(0,s+1);return t.concat(a)}),[])},_getDisjunctiveHierarchicalFacetAttribute:function(e,t,r){var n=e._getHierarchicalFacetSeparator(t);if(!0===r){var i=e._getHierarchicalRootPath(t),s=0;return i&&(s=i.split(n).length),[t.attributes[s]]}var a=(e.getHierarchicalRefinement(t.name)[0]||"").split(n).length-1;return t.attributes.slice(0,a+1)},getSearchForFacetQuery:function(e,t,r,a){var c=a.isDisjunctiveFacet(e)?a.clearRefinements(e):a,u={facetQuery:t,facetName:e};return"number"==typeof r&&(u.maxFacetHits=r),i(n({},s._getHitsSearchParams(c),u))}};e.exports=s},46801:e=>{"use strict";e.exports=function(e){return null!==e&&/^[a-zA-Z0-9_-]{1,64}$/.test(e)}},24336:e=>{"use strict";e.exports="3.15.0"},70290:function(e){e.exports=function(){"use strict";function e(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function t(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function r(r){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?t(Object(i),!0).forEach((function(t){e(r,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(r,Object.getOwnPropertyDescriptors(i)):t(Object(i)).forEach((function(e){Object.defineProperty(r,e,Object.getOwnPropertyDescriptor(i,e))}))}return r}function n(e,t){if(null==e)return{};var r,n,i=function(e,t){if(null==e)return{};var r,n,i={},s=Object.keys(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||(i[r]=e[r]);return i}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(i[r]=e[r])}return i}function i(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e)){var r=[],n=!0,i=!1,s=void 0;try{for(var a,c=e[Symbol.iterator]();!(n=(a=c.next()).done)&&(r.push(a.value),!t||r.length!==t);n=!0);}catch(e){i=!0,s=e}finally{try{n||null==c.return||c.return()}finally{if(i)throw s}}return r}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function s(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}function a(e){var t,r="algoliasearch-client-js-".concat(e.key),n=function(){return void 0===t&&(t=e.localStorage||window.localStorage),t},s=function(){return JSON.parse(n().getItem(r)||"{}")},a=function(e){n().setItem(r,JSON.stringify(e))},c=function(){var t=e.timeToLive?1e3*e.timeToLive:null,r=s(),n=Object.fromEntries(Object.entries(r).filter((function(e){return void 0!==i(e,2)[1].timestamp})));if(a(n),t){var c=Object.fromEntries(Object.entries(n).filter((function(e){var r=i(e,2)[1],n=(new Date).getTime();return!(r.timestamp+t<n)})));a(c)}};return{get:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return Promise.resolve().then((function(){c();var t=JSON.stringify(e);return s()[t]})).then((function(e){return Promise.all([e?e.value:t(),void 0!==e])})).then((function(e){var t=i(e,2),n=t[0],s=t[1];return Promise.all([n,s||r.miss(n)])})).then((function(e){return i(e,1)[0]}))},set:function(e,t){return Promise.resolve().then((function(){var i=s();return i[JSON.stringify(e)]={timestamp:(new Date).getTime(),value:t},n().setItem(r,JSON.stringify(i)),t}))},delete:function(e){return Promise.resolve().then((function(){var t=s();delete t[JSON.stringify(e)],n().setItem(r,JSON.stringify(t))}))},clear:function(){return Promise.resolve().then((function(){n().removeItem(r)}))}}}function c(e){var t=s(e.caches),r=t.shift();return void 0===r?{get:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return t().then((function(e){return Promise.all([e,r.miss(e)])})).then((function(e){return i(e,1)[0]}))},set:function(e,t){return Promise.resolve(t)},delete:function(e){return Promise.resolve()},clear:function(){return Promise.resolve()}}:{get:function(e,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return r.get(e,n,i).catch((function(){return c({caches:t}).get(e,n,i)}))},set:function(e,n){return r.set(e,n).catch((function(){return c({caches:t}).set(e,n)}))},delete:function(e){return r.delete(e).catch((function(){return c({caches:t}).delete(e)}))},clear:function(){return r.clear().catch((function(){return c({caches:t}).clear()}))}}}function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{serializable:!0},t={};return{get:function(r,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}},s=JSON.stringify(r);if(s in t)return Promise.resolve(e.serializable?JSON.parse(t[s]):t[s]);var a=n(),c=i&&i.miss||function(){return Promise.resolve()};return a.then((function(e){return c(e)})).then((function(){return a}))},set:function(r,n){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(n):n,Promise.resolve(n)},delete:function(e){return delete t[JSON.stringify(e)],Promise.resolve()},clear:function(){return t={},Promise.resolve()}}}function o(e){for(var t=e.length-1;t>0;t--){var r=Math.floor(Math.random()*(t+1)),n=e[t];e[t]=e[r],e[r]=n}return e}function h(e,t){return t?(Object.keys(t).forEach((function(r){e[r]=t[r](e)})),e):e}function f(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),n=1;n<t;n++)r[n-1]=arguments[n];var i=0;return e.replace(/%s/g,(function(){return encodeURIComponent(r[i++])}))}var l={WithinQueryParameters:0,WithinHeaders:1};function m(e,t){var r=e||{},n=r.data||{};return Object.keys(r).forEach((function(e){-1===["timeout","headers","queryParameters","data","cacheable"].indexOf(e)&&(n[e]=r[e])})),{data:Object.entries(n).length>0?n:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var d={Read:1,Write:2,Any:3},p=1,v=2,g=3;function y(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:p;return r(r({},e),{},{status:t,lastUpdate:Date.now()})}function R(e){return"string"==typeof e?{protocol:"https",url:e,accept:d.Any}:{protocol:e.protocol||"https",url:e.url,accept:e.accept||d.Any}}var F="GET",b="POST";function j(e,t){return Promise.all(t.map((function(t){return e.get(t,(function(){return Promise.resolve(y(t))}))}))).then((function(e){var r=e.filter((function(e){return function(e){return e.status===p||Date.now()-e.lastUpdate>12e4}(e)})),n=e.filter((function(e){return function(e){return e.status===g&&Date.now()-e.lastUpdate<=12e4}(e)})),i=[].concat(s(r),s(n));return{getTimeout:function(e,t){return(0===n.length&&0===e?1:n.length+3+e)*t},statelessHosts:i.length>0?i.map((function(e){return R(e)})):t}}))}function P(e,t,n,i){var a=[],c=function(e,t){if(e.method!==F&&(void 0!==e.data||void 0!==t.data)){var n=Array.isArray(e.data)?e.data:r(r({},e.data),t.data);return JSON.stringify(n)}}(n,i),u=function(e,t){var n=r(r({},e.headers),t.headers),i={};return Object.keys(n).forEach((function(e){var t=n[e];i[e.toLowerCase()]=t})),i}(e,i),o=n.method,h=n.method!==F?{}:r(r({},n.data),i.data),f=r(r(r({"x-algolia-agent":e.userAgent.value},e.queryParameters),h),i.queryParameters),l=0,m=function t(r,s){var h=r.pop();if(void 0===h)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:O(a)};var m={data:c,headers:u,method:o,url:_(h,n.path,f),connectTimeout:s(l,e.timeouts.connect),responseTimeout:s(l,i.timeout)},d=function(e){var t={request:m,response:e,host:h,triesLeft:r.length};return a.push(t),t},p={onSuccess:function(e){return function(e){try{return JSON.parse(e.content)}catch(t){throw function(e,t){return{name:"DeserializationError",message:e,response:t}}(t.message,e)}}(e)},onRetry:function(n){var i=d(n);return n.isTimedOut&&l++,Promise.all([e.logger.info("Retryable failure",w(i)),e.hostsCache.set(h,y(h,n.isTimedOut?g:v))]).then((function(){return t(r,s)}))},onFail:function(e){throw d(e),function(e,t){var r=e.content,n=e.status,i=r;try{i=JSON.parse(r).message}catch(e){}return function(e,t,r){return{name:"ApiError",message:e,status:t,transporterStackTrace:r}}(i,n,t)}(e,O(a))}};return e.requester.send(m).then((function(e){return function(e,t){return function(e){var t=e.status;return e.isTimedOut||function(e){var t=e.isTimedOut,r=e.status;return!t&&0==~~r}(e)||2!=~~(t/100)&&4!=~~(t/100)}(e)?t.onRetry(e):2==~~(e.status/100)?t.onSuccess(e):t.onFail(e)}(e,p)}))};return j(e.hostsCache,t).then((function(e){return m(s(e.statelessHosts).reverse(),e.getTimeout)}))}function x(e){var t={value:"Algolia for JavaScript (".concat(e,")"),add:function(e){var r="; ".concat(e.segment).concat(void 0!==e.version?" (".concat(e.version,")"):"");return-1===t.value.indexOf(r)&&(t.value="".concat(t.value).concat(r)),t}};return t}function _(e,t,r){var n=E(r),i="".concat(e.protocol,"://").concat(e.url,"/").concat("/"===t.charAt(0)?t.substr(1):t);return n.length&&(i+="?".concat(n)),i}function E(e){return Object.keys(e).map((function(t){return f("%s=%s",t,(r=e[t],"[object Object]"===Object.prototype.toString.call(r)||"[object Array]"===Object.prototype.toString.call(r)?JSON.stringify(e[t]):e[t]));var r})).join("&")}function O(e){return e.map((function(e){return w(e)}))}function w(e){var t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return r(r({},e),{},{request:r(r({},e.request),{},{headers:r(r({},e.request.headers),t)})})}var A=function(e){var t=e.appId,n=function(e,t,r){var n={"x-algolia-api-key":r,"x-algolia-application-id":t};return{headers:function(){return e===l.WithinHeaders?n:{}},queryParameters:function(){return e===l.WithinQueryParameters?n:{}}}}(void 0!==e.authMode?e.authMode:l.WithinHeaders,t,e.apiKey),s=function(e){var t=e.hostsCache,r=e.logger,n=e.requester,s=e.requestsCache,a=e.responsesCache,c=e.timeouts,u=e.userAgent,o=e.hosts,h=e.queryParameters,f={hostsCache:t,logger:r,requester:n,requestsCache:s,responsesCache:a,timeouts:c,userAgent:u,headers:e.headers,queryParameters:h,hosts:o.map((function(e){return R(e)})),read:function(e,t){var r=m(t,f.timeouts.read),n=function(){return P(f,f.hosts.filter((function(e){return 0!=(e.accept&d.Read)})),e,r)};if(!0!==(void 0!==r.cacheable?r.cacheable:e.cacheable))return n();var s={request:e,mappedRequestOptions:r,transporter:{queryParameters:f.queryParameters,headers:f.headers}};return f.responsesCache.get(s,(function(){return f.requestsCache.get(s,(function(){return f.requestsCache.set(s,n()).then((function(e){return Promise.all([f.requestsCache.delete(s),e])}),(function(e){return Promise.all([f.requestsCache.delete(s),Promise.reject(e)])})).then((function(e){var t=i(e,2);return t[0],t[1]}))}))}),{miss:function(e){return f.responsesCache.set(s,e)}})},write:function(e,t){return P(f,f.hosts.filter((function(e){return 0!=(e.accept&d.Write)})),e,m(t,f.timeouts.write))}};return f}(r(r({hosts:[{url:"".concat(t,"-dsn.algolia.net"),accept:d.Read},{url:"".concat(t,".algolia.net"),accept:d.Write}].concat(o([{url:"".concat(t,"-1.algolianet.com")},{url:"".concat(t,"-2.algolianet.com")},{url:"".concat(t,"-3.algolianet.com")}]))},e),{},{headers:r(r(r({},n.headers()),{"content-type":"application/x-www-form-urlencoded"}),e.headers),queryParameters:r(r({},n.queryParameters()),e.queryParameters)}));return h({transporter:s,appId:t,addAlgoliaAgent:function(e,t){s.userAgent.add({segment:e,version:t})},clearCache:function(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then((function(){}))}},e.methods)},N=function(e){return function(t,r){return t.method===F?e.transporter.read(t,r):e.transporter.write(t,r)}},H=function(e){return function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return h({transporter:e.transporter,appId:e.appId,indexName:t},r.methods)}},S=function(e){return function(t,n){var i=t.map((function(e){return r(r({},e),{},{params:E(e.params||{})})}));return e.transporter.read({method:b,path:"1/indexes/*/queries",data:{requests:i},cacheable:!0},n)}},T=function(e){return function(t,i){return Promise.all(t.map((function(t){var s=t.params,a=s.facetName,c=s.facetQuery,u=n(s,["facetName","facetQuery"]);return H(e)(t.indexName,{methods:{searchForFacetValues:I}}).searchForFacetValues(a,c,r(r({},i),u))})))}},Q=function(e){return function(t,r,n){return e.transporter.read({method:b,path:f("1/answers/%s/prediction",e.indexName),data:{query:t,queryLanguages:r},cacheable:!0},n)}},C=function(e){return function(t,r){return e.transporter.read({method:b,path:f("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},r)}},I=function(e){return function(t,r,n){return e.transporter.read({method:b,path:f("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:r},cacheable:!0},n)}},D=1,k=2,q=3;function V(e,t,n){var i,s={appId:e,apiKey:t,timeouts:{connect:1,read:2,write:30},requester:{send:function(e){return new Promise((function(t){var r=new XMLHttpRequest;r.open(e.method,e.url,!0),Object.keys(e.headers).forEach((function(t){return r.setRequestHeader(t,e.headers[t])}));var n,i=function(e,n){return setTimeout((function(){r.abort(),t({status:0,content:n,isTimedOut:!0})}),1e3*e)},s=i(e.connectTimeout,"Connection timeout");r.onreadystatechange=function(){r.readyState>r.OPENED&&void 0===n&&(clearTimeout(s),n=i(e.responseTimeout,"Socket timeout"))},r.onerror=function(){0===r.status&&(clearTimeout(s),clearTimeout(n),t({content:r.responseText||"Network request failed",status:r.status,isTimedOut:!1}))},r.onload=function(){clearTimeout(s),clearTimeout(n),t({content:r.responseText,status:r.status,isTimedOut:!1})},r.send(e.data)}))}},logger:(i=q,{debug:function(e,t){return D>=i&&console.debug(e,t),Promise.resolve()},info:function(e,t){return k>=i&&console.info(e,t),Promise.resolve()},error:function(e,t){return console.error(e,t),Promise.resolve()}}),responsesCache:u(),requestsCache:u({serializable:!1}),hostsCache:c({caches:[a({key:"".concat("4.20.0","-").concat(e)}),u()]}),userAgent:x("4.20.0").add({segment:"Browser",version:"lite"}),authMode:l.WithinQueryParameters};return A(r(r(r({},s),n),{},{methods:{search:S,searchForFacetValues:T,multipleQueries:S,multipleSearchForFacetValues:T,customRequest:N,initIndex:function(e){return function(t){return H(e)(t,{methods:{search:C,searchForFacetValues:I,findAnswers:Q}})}}}}))}return V.version="4.20.0",V}()}}]); \ No newline at end of file diff --git a/assets/js/1a4e3797.9c47db27.js b/assets/js/1a4e3797.9c47db27.js new file mode 100644 index 000000000..9b086265f --- /dev/null +++ b/assets/js/1a4e3797.9c47db27.js @@ -0,0 +1,2 @@ +/*! For license information please see 1a4e3797.9c47db27.js.LICENSE.txt */ +(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7920],{17331:e=>{function t(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function r(e){return"function"==typeof e}function n(e){return"object"==typeof e&&null!==e}function i(e){return void 0===e}e.exports=t,t.prototype._events=void 0,t.prototype._maxListeners=void 0,t.defaultMaxListeners=10,t.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},t.prototype.emit=function(e){var t,s,a,c,u,o;if(this._events||(this._events={}),"error"===e&&(!this._events.error||n(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var h=new Error('Uncaught, unspecified "error" event. ('+t+")");throw h.context=t,h}if(i(s=this._events[e]))return!1;if(r(s))switch(arguments.length){case 1:s.call(this);break;case 2:s.call(this,arguments[1]);break;case 3:s.call(this,arguments[1],arguments[2]);break;default:c=Array.prototype.slice.call(arguments,1),s.apply(this,c)}else if(n(s))for(c=Array.prototype.slice.call(arguments,1),a=(o=s.slice()).length,u=0;u<a;u++)o[u].apply(this,c);return!0},t.prototype.addListener=function(e,s){var a;if(!r(s))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,r(s.listener)?s.listener:s),this._events[e]?n(this._events[e])?this._events[e].push(s):this._events[e]=[this._events[e],s]:this._events[e]=s,n(this._events[e])&&!this._events[e].warned&&(a=i(this._maxListeners)?t.defaultMaxListeners:this._maxListeners)&&a>0&&this._events[e].length>a&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},t.prototype.on=t.prototype.addListener,t.prototype.once=function(e,t){if(!r(t))throw TypeError("listener must be a function");var n=!1;function i(){this.removeListener(e,i),n||(n=!0,t.apply(this,arguments))}return i.listener=t,this.on(e,i),this},t.prototype.removeListener=function(e,t){var i,s,a,c;if(!r(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(a=(i=this._events[e]).length,s=-1,i===t||r(i.listener)&&i.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(n(i)){for(c=a;c-- >0;)if(i[c]===t||i[c].listener&&i[c].listener===t){s=c;break}if(s<0)return this;1===i.length?(i.length=0,delete this._events[e]):i.splice(s,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},t.prototype.removeAllListeners=function(e){var t,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r(n=this._events[e]))this.removeListener(e,n);else if(n)for(;n.length;)this.removeListener(e,n[n.length-1]);return delete this._events[e],this},t.prototype.listeners=function(e){return this._events&&this._events[e]?r(this._events[e])?[this._events[e]]:this._events[e].slice():[]},t.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(r(t))return 1;if(t)return t.length}return 0},t.listenerCount=function(e,t){return e.listenerCount(t)}},8131:(e,t,r)=>{"use strict";var n=r(49374),i=r(17775),s=r(23076);function a(e,t,r){return new n(e,t,r)}a.version=r(24336),a.AlgoliaSearchHelper=n,a.SearchParameters=i,a.SearchResults=s,e.exports=a},68078:(e,t,r)=>{"use strict";var n=r(17331);function i(e,t){this.main=e,this.fn=t,this.lastResults=null}r(14853)(i,n),i.prototype.detach=function(){this.removeAllListeners(),this.main.detachDerivedHelper(this)},i.prototype.getModifiedState=function(e){return this.fn(e)},e.exports=i},82437:(e,t,r)=>{"use strict";var n=r(52344),i=r(90116),s=r(49803),a={addRefinement:function(e,t,r){if(a.isRefined(e,t,r))return e;var i=""+r,s=e[t]?e[t].concat(i):[i],c={};return c[t]=s,n({},c,e)},removeRefinement:function(e,t,r){if(void 0===r)return a.clearRefinement(e,(function(e,r){return t===r}));var n=""+r;return a.clearRefinement(e,(function(e,r){return t===r&&n===e}))},toggleRefinement:function(e,t,r){if(void 0===r)throw new Error("toggleRefinement should be used with a value");return a.isRefined(e,t,r)?a.removeRefinement(e,t,r):a.addRefinement(e,t,r)},clearRefinement:function(e,t,r){if(void 0===t)return i(e)?{}:e;if("string"==typeof t)return s(e,[t]);if("function"==typeof t){var n=!1,a=Object.keys(e).reduce((function(i,s){var a=e[s]||[],c=a.filter((function(e){return!t(e,s,r)}));return c.length!==a.length&&(n=!0),i[s]=c,i}),{});return n?a:e}},isRefined:function(e,t,r){var n=Boolean(e[t])&&e[t].length>0;if(void 0===r||!n)return n;var i=""+r;return-1!==e[t].indexOf(i)}};e.exports=a},17775:(e,t,r)=>{"use strict";var n=r(52344),i=r(7888),s=r(22686),a=r(60185),c=r(90116),u=r(49803),o=r(28023),h=r(46801),f=r(82437);function l(e,t){return Array.isArray(e)&&Array.isArray(t)?e.length===t.length&&e.every((function(e,r){return l(t[r],e)})):e===t}function m(e){var t=e?m._parseNumbers(e):{};void 0===t.userToken||h(t.userToken)||console.warn("[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}"),this.facets=t.facets||[],this.disjunctiveFacets=t.disjunctiveFacets||[],this.hierarchicalFacets=t.hierarchicalFacets||[],this.facetsRefinements=t.facetsRefinements||{},this.facetsExcludes=t.facetsExcludes||{},this.disjunctiveFacetsRefinements=t.disjunctiveFacetsRefinements||{},this.numericRefinements=t.numericRefinements||{},this.tagRefinements=t.tagRefinements||[],this.hierarchicalFacetsRefinements=t.hierarchicalFacetsRefinements||{};var r=this;Object.keys(t).forEach((function(e){var n=-1!==m.PARAMETERS.indexOf(e),i=void 0!==t[e];!n&&i&&(r[e]=t[e])}))}m.PARAMETERS=Object.keys(new m),m._parseNumbers=function(e){if(e instanceof m)return e;var t={};if(["aroundPrecision","aroundRadius","getRankingInfo","minWordSizefor2Typos","minWordSizefor1Typo","page","maxValuesPerFacet","distinct","minimumAroundRadius","hitsPerPage","minProximity"].forEach((function(r){var n=e[r];if("string"==typeof n){var i=parseFloat(n);t[r]=isNaN(i)?n:i}})),Array.isArray(e.insideBoundingBox)&&(t.insideBoundingBox=e.insideBoundingBox.map((function(e){return Array.isArray(e)?e.map((function(e){return parseFloat(e)})):e}))),e.numericRefinements){var r={};Object.keys(e.numericRefinements).forEach((function(t){var n=e.numericRefinements[t]||{};r[t]={},Object.keys(n).forEach((function(e){var i=n[e].map((function(e){return Array.isArray(e)?e.map((function(e){return"string"==typeof e?parseFloat(e):e})):"string"==typeof e?parseFloat(e):e}));r[t][e]=i}))})),t.numericRefinements=r}return a({},e,t)},m.make=function(e){var t=new m(e);return(e.hierarchicalFacets||[]).forEach((function(e){if(e.rootPath){var r=t.getHierarchicalRefinement(e.name);r.length>0&&0!==r[0].indexOf(e.rootPath)&&(t=t.clearRefinements(e.name)),0===(r=t.getHierarchicalRefinement(e.name)).length&&(t=t.toggleHierarchicalFacetRefinement(e.name,e.rootPath))}})),t},m.validate=function(e,t){var r=t||{};return e.tagFilters&&r.tagRefinements&&r.tagRefinements.length>0?new Error("[Tags] Cannot switch from the managed tag API to the advanced API. It is probably an error, if it is really what you want, you should first clear the tags with clearTags method."):e.tagRefinements.length>0&&r.tagFilters?new Error("[Tags] Cannot switch from the advanced tag API to the managed API. It is probably an error, if it is not, you should first clear the tags with clearTags method."):e.numericFilters&&r.numericRefinements&&c(r.numericRefinements)?new Error("[Numeric filters] Can't switch from the advanced to the managed API. It is probably an error, if this is really what you want, you have to first clear the numeric filters."):c(e.numericRefinements)&&r.numericFilters?new Error("[Numeric filters] Can't switch from the managed API to the advanced. It is probably an error, if this is really what you want, you have to first clear the numeric filters."):null},m.prototype={constructor:m,clearRefinements:function(e){var t={numericRefinements:this._clearNumericRefinements(e),facetsRefinements:f.clearRefinement(this.facetsRefinements,e,"conjunctiveFacet"),facetsExcludes:f.clearRefinement(this.facetsExcludes,e,"exclude"),disjunctiveFacetsRefinements:f.clearRefinement(this.disjunctiveFacetsRefinements,e,"disjunctiveFacet"),hierarchicalFacetsRefinements:f.clearRefinement(this.hierarchicalFacetsRefinements,e,"hierarchicalFacet")};return t.numericRefinements===this.numericRefinements&&t.facetsRefinements===this.facetsRefinements&&t.facetsExcludes===this.facetsExcludes&&t.disjunctiveFacetsRefinements===this.disjunctiveFacetsRefinements&&t.hierarchicalFacetsRefinements===this.hierarchicalFacetsRefinements?this:this.setQueryParameters(t)},clearTags:function(){return void 0===this.tagFilters&&0===this.tagRefinements.length?this:this.setQueryParameters({tagFilters:void 0,tagRefinements:[]})},setIndex:function(e){return e===this.index?this:this.setQueryParameters({index:e})},setQuery:function(e){return e===this.query?this:this.setQueryParameters({query:e})},setPage:function(e){return e===this.page?this:this.setQueryParameters({page:e})},setFacets:function(e){return this.setQueryParameters({facets:e})},setDisjunctiveFacets:function(e){return this.setQueryParameters({disjunctiveFacets:e})},setHitsPerPage:function(e){return this.hitsPerPage===e?this:this.setQueryParameters({hitsPerPage:e})},setTypoTolerance:function(e){return this.typoTolerance===e?this:this.setQueryParameters({typoTolerance:e})},addNumericRefinement:function(e,t,r){var n=o(r);if(this.isNumericRefined(e,t,n))return this;var i=a({},this.numericRefinements);return i[e]=a({},i[e]),i[e][t]?(i[e][t]=i[e][t].slice(),i[e][t].push(n)):i[e][t]=[n],this.setQueryParameters({numericRefinements:i})},getConjunctiveRefinements:function(e){return this.isConjunctiveFacet(e)&&this.facetsRefinements[e]||[]},getDisjunctiveRefinements:function(e){return this.isDisjunctiveFacet(e)&&this.disjunctiveFacetsRefinements[e]||[]},getHierarchicalRefinement:function(e){return this.hierarchicalFacetsRefinements[e]||[]},getExcludeRefinements:function(e){return this.isConjunctiveFacet(e)&&this.facetsExcludes[e]||[]},removeNumericRefinement:function(e,t,r){var n=r;return void 0!==n?this.isNumericRefined(e,t,n)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(r,i){return i===e&&r.op===t&&l(r.val,o(n))}))}):this:void 0!==t?this.isNumericRefined(e,t)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(r,n){return n===e&&r.op===t}))}):this:this.isNumericRefined(e)?this.setQueryParameters({numericRefinements:this._clearNumericRefinements((function(t,r){return r===e}))}):this},getNumericRefinements:function(e){return this.numericRefinements[e]||{}},getNumericRefinement:function(e,t){return this.numericRefinements[e]&&this.numericRefinements[e][t]},_clearNumericRefinements:function(e){if(void 0===e)return c(this.numericRefinements)?{}:this.numericRefinements;if("string"==typeof e)return u(this.numericRefinements,[e]);if("function"==typeof e){var t=!1,r=this.numericRefinements,n=Object.keys(r).reduce((function(n,i){var s=r[i],a={};return s=s||{},Object.keys(s).forEach((function(r){var n=s[r]||[],c=[];n.forEach((function(t){e({val:t,op:r},i,"numeric")||c.push(t)})),c.length!==n.length&&(t=!0),a[r]=c})),n[i]=a,n}),{});return t?n:this.numericRefinements}},addFacet:function(e){return this.isConjunctiveFacet(e)?this:this.setQueryParameters({facets:this.facets.concat([e])})},addDisjunctiveFacet:function(e){return this.isDisjunctiveFacet(e)?this:this.setQueryParameters({disjunctiveFacets:this.disjunctiveFacets.concat([e])})},addHierarchicalFacet:function(e){if(this.isHierarchicalFacet(e.name))throw new Error("Cannot declare two hierarchical facets with the same name: `"+e.name+"`");return this.setQueryParameters({hierarchicalFacets:this.hierarchicalFacets.concat([e])})},addFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsRefinements,e,t)?this:this.setQueryParameters({facetsRefinements:f.addRefinement(this.facetsRefinements,e,t)})},addExcludeRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsExcludes,e,t)?this:this.setQueryParameters({facetsExcludes:f.addRefinement(this.facetsExcludes,e,t)})},addDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return f.isRefined(this.disjunctiveFacetsRefinements,e,t)?this:this.setQueryParameters({disjunctiveFacetsRefinements:f.addRefinement(this.disjunctiveFacetsRefinements,e,t)})},addTagRefinement:function(e){if(this.isTagRefined(e))return this;var t={tagRefinements:this.tagRefinements.concat(e)};return this.setQueryParameters(t)},removeFacet:function(e){return this.isConjunctiveFacet(e)?this.clearRefinements(e).setQueryParameters({facets:this.facets.filter((function(t){return t!==e}))}):this},removeDisjunctiveFacet:function(e){return this.isDisjunctiveFacet(e)?this.clearRefinements(e).setQueryParameters({disjunctiveFacets:this.disjunctiveFacets.filter((function(t){return t!==e}))}):this},removeHierarchicalFacet:function(e){return this.isHierarchicalFacet(e)?this.clearRefinements(e).setQueryParameters({hierarchicalFacets:this.hierarchicalFacets.filter((function(t){return t.name!==e}))}):this},removeFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsRefinements,e,t)?this.setQueryParameters({facetsRefinements:f.removeRefinement(this.facetsRefinements,e,t)}):this},removeExcludeRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return f.isRefined(this.facetsExcludes,e,t)?this.setQueryParameters({facetsExcludes:f.removeRefinement(this.facetsExcludes,e,t)}):this},removeDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return f.isRefined(this.disjunctiveFacetsRefinements,e,t)?this.setQueryParameters({disjunctiveFacetsRefinements:f.removeRefinement(this.disjunctiveFacetsRefinements,e,t)}):this},removeTagRefinement:function(e){if(!this.isTagRefined(e))return this;var t={tagRefinements:this.tagRefinements.filter((function(t){return t!==e}))};return this.setQueryParameters(t)},toggleRefinement:function(e,t){return this.toggleFacetRefinement(e,t)},toggleFacetRefinement:function(e,t){if(this.isHierarchicalFacet(e))return this.toggleHierarchicalFacetRefinement(e,t);if(this.isConjunctiveFacet(e))return this.toggleConjunctiveFacetRefinement(e,t);if(this.isDisjunctiveFacet(e))return this.toggleDisjunctiveFacetRefinement(e,t);throw new Error("Cannot refine the undeclared facet "+e+"; it should be added to the helper options facets, disjunctiveFacets or hierarchicalFacets")},toggleConjunctiveFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return this.setQueryParameters({facetsRefinements:f.toggleRefinement(this.facetsRefinements,e,t)})},toggleExcludeFacetRefinement:function(e,t){if(!this.isConjunctiveFacet(e))throw new Error(e+" is not defined in the facets attribute of the helper configuration");return this.setQueryParameters({facetsExcludes:f.toggleRefinement(this.facetsExcludes,e,t)})},toggleDisjunctiveFacetRefinement:function(e,t){if(!this.isDisjunctiveFacet(e))throw new Error(e+" is not defined in the disjunctiveFacets attribute of the helper configuration");return this.setQueryParameters({disjunctiveFacetsRefinements:f.toggleRefinement(this.disjunctiveFacetsRefinements,e,t)})},toggleHierarchicalFacetRefinement:function(e,t){if(!this.isHierarchicalFacet(e))throw new Error(e+" is not defined in the hierarchicalFacets attribute of the helper configuration");var r=this._getHierarchicalFacetSeparator(this.getHierarchicalFacetByName(e)),i={};return void 0!==this.hierarchicalFacetsRefinements[e]&&this.hierarchicalFacetsRefinements[e].length>0&&(this.hierarchicalFacetsRefinements[e][0]===t||0===this.hierarchicalFacetsRefinements[e][0].indexOf(t+r))?-1===t.indexOf(r)?i[e]=[]:i[e]=[t.slice(0,t.lastIndexOf(r))]:i[e]=[t],this.setQueryParameters({hierarchicalFacetsRefinements:n({},i,this.hierarchicalFacetsRefinements)})},addHierarchicalFacetRefinement:function(e,t){if(this.isHierarchicalFacetRefined(e))throw new Error(e+" is already refined.");if(!this.isHierarchicalFacet(e))throw new Error(e+" is not defined in the hierarchicalFacets attribute of the helper configuration.");var r={};return r[e]=[t],this.setQueryParameters({hierarchicalFacetsRefinements:n({},r,this.hierarchicalFacetsRefinements)})},removeHierarchicalFacetRefinement:function(e){if(!this.isHierarchicalFacetRefined(e))return this;var t={};return t[e]=[],this.setQueryParameters({hierarchicalFacetsRefinements:n({},t,this.hierarchicalFacetsRefinements)})},toggleTagRefinement:function(e){return this.isTagRefined(e)?this.removeTagRefinement(e):this.addTagRefinement(e)},isDisjunctiveFacet:function(e){return this.disjunctiveFacets.indexOf(e)>-1},isHierarchicalFacet:function(e){return void 0!==this.getHierarchicalFacetByName(e)},isConjunctiveFacet:function(e){return this.facets.indexOf(e)>-1},isFacetRefined:function(e,t){return!!this.isConjunctiveFacet(e)&&f.isRefined(this.facetsRefinements,e,t)},isExcludeRefined:function(e,t){return!!this.isConjunctiveFacet(e)&&f.isRefined(this.facetsExcludes,e,t)},isDisjunctiveFacetRefined:function(e,t){return!!this.isDisjunctiveFacet(e)&&f.isRefined(this.disjunctiveFacetsRefinements,e,t)},isHierarchicalFacetRefined:function(e,t){if(!this.isHierarchicalFacet(e))return!1;var r=this.getHierarchicalRefinement(e);return t?-1!==r.indexOf(t):r.length>0},isNumericRefined:function(e,t,r){if(void 0===r&&void 0===t)return Boolean(this.numericRefinements[e]);var n=this.numericRefinements[e]&&void 0!==this.numericRefinements[e][t];if(void 0===r||!n)return n;var s,a,c=o(r),u=void 0!==(s=this.numericRefinements[e][t],a=c,i(s,(function(e){return l(e,a)})));return n&&u},isTagRefined:function(e){return-1!==this.tagRefinements.indexOf(e)},getRefinedDisjunctiveFacets:function(){var e=this,t=s(Object.keys(this.numericRefinements).filter((function(t){return Object.keys(e.numericRefinements[t]).length>0})),this.disjunctiveFacets);return Object.keys(this.disjunctiveFacetsRefinements).filter((function(t){return e.disjunctiveFacetsRefinements[t].length>0})).concat(t).concat(this.getRefinedHierarchicalFacets()).sort()},getRefinedHierarchicalFacets:function(){var e=this;return s(this.hierarchicalFacets.map((function(e){return e.name})),Object.keys(this.hierarchicalFacetsRefinements).filter((function(t){return e.hierarchicalFacetsRefinements[t].length>0}))).sort()},getUnrefinedDisjunctiveFacets:function(){var e=this.getRefinedDisjunctiveFacets();return this.disjunctiveFacets.filter((function(t){return-1===e.indexOf(t)}))},managedParameters:["index","facets","disjunctiveFacets","facetsRefinements","hierarchicalFacets","facetsExcludes","disjunctiveFacetsRefinements","numericRefinements","tagRefinements","hierarchicalFacetsRefinements"],getQueryParams:function(){var e=this.managedParameters,t={},r=this;return Object.keys(this).forEach((function(n){var i=r[n];-1===e.indexOf(n)&&void 0!==i&&(t[n]=i)})),t},setQueryParameter:function(e,t){if(this[e]===t)return this;var r={};return r[e]=t,this.setQueryParameters(r)},setQueryParameters:function(e){if(!e)return this;var t=m.validate(this,e);if(t)throw t;var r=this,n=m._parseNumbers(e),i=Object.keys(this).reduce((function(e,t){return e[t]=r[t],e}),{}),s=Object.keys(n).reduce((function(e,t){var r=void 0!==e[t],i=void 0!==n[t];return r&&!i?u(e,[t]):(i&&(e[t]=n[t]),e)}),i);return new this.constructor(s)},resetPage:function(){return void 0===this.page?this:this.setPage(0)},_getHierarchicalFacetSortBy:function(e){return e.sortBy||["isRefined:desc","name:asc"]},_getHierarchicalFacetSeparator:function(e){return e.separator||" > "},_getHierarchicalRootPath:function(e){return e.rootPath||null},_getHierarchicalShowParentLevel:function(e){return"boolean"!=typeof e.showParentLevel||e.showParentLevel},getHierarchicalFacetByName:function(e){return i(this.hierarchicalFacets,(function(t){return t.name===e}))},getHierarchicalFacetBreadcrumb:function(e){if(!this.isHierarchicalFacet(e))return[];var t=this.getHierarchicalRefinement(e)[0];if(!t)return[];var r=this._getHierarchicalFacetSeparator(this.getHierarchicalFacetByName(e));return t.split(r).map((function(e){return e.trim()}))},toString:function(){return JSON.stringify(this,null,2)}},e.exports=m},10210:(e,t,r)=>{"use strict";e.exports=function(e){return function(t,r){var n=e.hierarchicalFacets[r],o=e.hierarchicalFacetsRefinements[n.name]&&e.hierarchicalFacetsRefinements[n.name][0]||"",h=e._getHierarchicalFacetSeparator(n),f=e._getHierarchicalRootPath(n),l=e._getHierarchicalShowParentLevel(n),m=s(e._getHierarchicalFacetSortBy(n)),d=t.every((function(e){return e.exhaustive})),p=function(e,t,r,n,s){return function(o,h,f){var l=o;if(f>0){var m=0;for(l=o;m<f;){var d=l&&Array.isArray(l.data)?l.data:[];l=i(d,(function(e){return e.isRefined})),m++}}if(l){var p=Object.keys(h.data).map((function(e){return[e,h.data[e]]})).filter((function(e){return function(e,t,r,n,i,s){if(i&&(0!==e.indexOf(i)||i===e))return!1;return!i&&-1===e.indexOf(n)||i&&e.split(n).length-i.split(n).length==1||-1===e.indexOf(n)&&-1===r.indexOf(n)||0===r.indexOf(e)||0===e.indexOf(t+n)&&(s||0===e.indexOf(r))}(e[0],l.path||r,s,t,r,n)}));l.data=a(p.map((function(e){var r=e[0];return function(e,t,r,n,i){var s=t.split(r);return{name:s[s.length-1].trim(),path:t,escapedValue:c(t),count:e,isRefined:n===t||0===n.indexOf(t+r),exhaustive:i,data:null}}(e[1],r,t,u(s),h.exhaustive)})),e[0],e[1])}return o}}(m,h,f,l,o),v=t;return f&&(v=t.slice(f.split(h).length)),v.reduce(p,{name:e.hierarchicalFacets[r].name,count:null,isRefined:!0,path:null,escapedValue:null,exhaustive:d,data:null})}};var n=r(94039),i=r(7888),s=r(82293),a=r(42148),c=n.escapeFacetValue,u=n.unescapeFacetValue},23076:(e,t,r)=>{"use strict";var n=r(74587),i=r(52344),s=r(94039),a=r(7888),c=r(69725),u=r(82293),o=r(60185),h=r(42148),f=s.escapeFacetValue,l=s.unescapeFacetValue,m=r(10210);function d(e){var t={};return e.forEach((function(e,r){t[e]=r})),t}function p(e,t,r){t&&t[r]&&(e.stats=t[r])}function v(e,t,r){var s=t[0];this._rawResults=t;var u=this;Object.keys(s).forEach((function(e){u[e]=s[e]})),Object.keys(r||{}).forEach((function(e){u[e]=r[e]})),this.processingTimeMS=t.reduce((function(e,t){return void 0===t.processingTimeMS?e:e+t.processingTimeMS}),0),this.disjunctiveFacets=[],this.hierarchicalFacets=e.hierarchicalFacets.map((function(){return[]})),this.facets=[];var h=e.getRefinedDisjunctiveFacets(),f=d(e.facets),v=d(e.disjunctiveFacets),g=1,y=s.facets||{};Object.keys(y).forEach((function(t){var r,n,i=y[t],o=(r=e.hierarchicalFacets,n=t,a(r,(function(e){return(e.attributes||[]).indexOf(n)>-1})));if(o){var h=o.attributes.indexOf(t),l=c(e.hierarchicalFacets,(function(e){return e.name===o.name}));u.hierarchicalFacets[l][h]={attribute:t,data:i,exhaustive:s.exhaustiveFacetsCount}}else{var m,d=-1!==e.disjunctiveFacets.indexOf(t),g=-1!==e.facets.indexOf(t);d&&(m=v[t],u.disjunctiveFacets[m]={name:t,data:i,exhaustive:s.exhaustiveFacetsCount},p(u.disjunctiveFacets[m],s.facets_stats,t)),g&&(m=f[t],u.facets[m]={name:t,data:i,exhaustive:s.exhaustiveFacetsCount},p(u.facets[m],s.facets_stats,t))}})),this.hierarchicalFacets=n(this.hierarchicalFacets),h.forEach((function(r){var n=t[g],a=n&&n.facets?n.facets:{},h=e.getHierarchicalFacetByName(r);Object.keys(a).forEach((function(t){var r,f=a[t];if(h){r=c(e.hierarchicalFacets,(function(e){return e.name===h.name}));var m=c(u.hierarchicalFacets[r],(function(e){return e.attribute===t}));if(-1===m)return;u.hierarchicalFacets[r][m].data=o({},u.hierarchicalFacets[r][m].data,f)}else{r=v[t];var d=s.facets&&s.facets[t]||{};u.disjunctiveFacets[r]={name:t,data:i({},f,d),exhaustive:n.exhaustiveFacetsCount},p(u.disjunctiveFacets[r],n.facets_stats,t),e.disjunctiveFacetsRefinements[t]&&e.disjunctiveFacetsRefinements[t].forEach((function(n){!u.disjunctiveFacets[r].data[n]&&e.disjunctiveFacetsRefinements[t].indexOf(l(n))>-1&&(u.disjunctiveFacets[r].data[n]=0)}))}})),g++})),e.getRefinedHierarchicalFacets().forEach((function(r){var n=e.getHierarchicalFacetByName(r),s=e._getHierarchicalFacetSeparator(n),a=e.getHierarchicalRefinement(r);0===a.length||a[0].split(s).length<2||t.slice(g).forEach((function(t){var r=t&&t.facets?t.facets:{};Object.keys(r).forEach((function(t){var o=r[t],h=c(e.hierarchicalFacets,(function(e){return e.name===n.name})),f=c(u.hierarchicalFacets[h],(function(e){return e.attribute===t}));if(-1!==f){var l={};if(a.length>0){var m=a[0].split(s)[0];l[m]=u.hierarchicalFacets[h][f].data[m]}u.hierarchicalFacets[h][f].data=i(l,o,u.hierarchicalFacets[h][f].data)}})),g++}))})),Object.keys(e.facetsExcludes).forEach((function(t){var r=e.facetsExcludes[t],n=f[t];u.facets[n]={name:t,data:y[t],exhaustive:s.exhaustiveFacetsCount},r.forEach((function(e){u.facets[n]=u.facets[n]||{name:t},u.facets[n].data=u.facets[n].data||{},u.facets[n].data[e]=0}))})),this.hierarchicalFacets=this.hierarchicalFacets.map(m(e)),this.facets=n(this.facets),this.disjunctiveFacets=n(this.disjunctiveFacets),this._state=e}function g(e,t){function r(e){return e.name===t}if(e._state.isConjunctiveFacet(t)){var n=a(e.facets,r);return n?Object.keys(n.data).map((function(r){var i=f(r);return{name:r,escapedValue:i,count:n.data[r],isRefined:e._state.isFacetRefined(t,i),isExcluded:e._state.isExcludeRefined(t,r)}})):[]}if(e._state.isDisjunctiveFacet(t)){var i=a(e.disjunctiveFacets,r);return i?Object.keys(i.data).map((function(r){var n=f(r);return{name:r,escapedValue:n,count:i.data[r],isRefined:e._state.isDisjunctiveFacetRefined(t,n)}})):[]}if(e._state.isHierarchicalFacet(t)){var s=a(e.hierarchicalFacets,r);if(!s)return s;var c=e._state.getHierarchicalFacetByName(t),u=e._state._getHierarchicalFacetSeparator(c),o=l(e._state.getHierarchicalRefinement(t)[0]||"");0===o.indexOf(c.rootPath)&&(o=o.replace(c.rootPath+u,""));var h=o.split(u);return h.unshift(t),y(s,h,0),s}}function y(e,t,r){e.isRefined=e.name===t[r],e.data&&e.data.forEach((function(e){y(e,t,r+1)}))}function R(e,t,r,n){if(n=n||0,Array.isArray(t))return e(t,r[n]);if(!t.data||0===t.data.length)return t;var s=t.data.map((function(t){return R(e,t,r,n+1)})),a=e(s,r[n]);return i({data:a},t)}function F(e,t){var r=a(e,(function(e){return e.name===t}));return r&&r.stats}function b(e,t,r,n,i){var s=a(i,(function(e){return e.name===r})),c=s&&s.data&&s.data[n]?s.data[n]:0,u=s&&s.exhaustive||!1;return{type:t,attributeName:r,name:n,count:c,exhaustive:u}}v.prototype.getFacetByName=function(e){function t(t){return t.name===e}return a(this.facets,t)||a(this.disjunctiveFacets,t)||a(this.hierarchicalFacets,t)},v.DEFAULT_SORT=["isRefined:desc","count:desc","name:asc"],v.prototype.getFacetValues=function(e,t){var r=g(this,e);if(r){var n,s=i({},t,{sortBy:v.DEFAULT_SORT,facetOrdering:!(t&&t.sortBy)}),a=this;if(Array.isArray(r))n=[e];else n=a._state.getHierarchicalFacetByName(r.name).attributes;return R((function(e,t){if(s.facetOrdering){var r=function(e,t){return e.renderingContent&&e.renderingContent.facetOrdering&&e.renderingContent.facetOrdering.values&&e.renderingContent.facetOrdering.values[t]}(a,t);if(r)return function(e,t){var r=[],n=[],i=(t.order||[]).reduce((function(e,t,r){return e[t]=r,e}),{});e.forEach((function(e){var t=e.path||e.name;void 0!==i[t]?r[i[t]]=e:n.push(e)})),r=r.filter((function(e){return e}));var s,a=t.sortRemainingBy;return"hidden"===a?r:(s="alpha"===a?[["path","name"],["asc","asc"]]:[["count"],["desc"]],r.concat(h(n,s[0],s[1])))}(e,r)}if(Array.isArray(s.sortBy)){var n=u(s.sortBy,v.DEFAULT_SORT);return h(e,n[0],n[1])}if("function"==typeof s.sortBy)return function(e,t){return t.sort(e)}(s.sortBy,e);throw new Error("options.sortBy is optional but if defined it must be either an array of string (predicates) or a sorting function")}),r,n)}},v.prototype.getFacetStats=function(e){return this._state.isConjunctiveFacet(e)?F(this.facets,e):this._state.isDisjunctiveFacet(e)?F(this.disjunctiveFacets,e):void 0},v.prototype.getRefinements=function(){var e=this._state,t=this,r=[];return Object.keys(e.facetsRefinements).forEach((function(n){e.facetsRefinements[n].forEach((function(i){r.push(b(e,"facet",n,i,t.facets))}))})),Object.keys(e.facetsExcludes).forEach((function(n){e.facetsExcludes[n].forEach((function(i){r.push(b(e,"exclude",n,i,t.facets))}))})),Object.keys(e.disjunctiveFacetsRefinements).forEach((function(n){e.disjunctiveFacetsRefinements[n].forEach((function(i){r.push(b(e,"disjunctive",n,i,t.disjunctiveFacets))}))})),Object.keys(e.hierarchicalFacetsRefinements).forEach((function(n){e.hierarchicalFacetsRefinements[n].forEach((function(i){r.push(function(e,t,r,n){var i=e.getHierarchicalFacetByName(t),s=e._getHierarchicalFacetSeparator(i),c=r.split(s),u=a(n,(function(e){return e.name===t})),o=c.reduce((function(e,t){var r=e&&a(e.data,(function(e){return e.name===t}));return void 0!==r?r:e}),u),h=o&&o.count||0,f=o&&o.exhaustive||!1,l=o&&o.path||"";return{type:"hierarchical",attributeName:t,name:l,count:h,exhaustive:f}}(e,n,i,t.hierarchicalFacets))}))})),Object.keys(e.numericRefinements).forEach((function(t){var n=e.numericRefinements[t];Object.keys(n).forEach((function(e){n[e].forEach((function(n){r.push({type:"numeric",attributeName:t,name:n,numericValue:n,operator:e})}))}))})),e.tagRefinements.forEach((function(e){r.push({type:"tag",attributeName:"_tags",name:e})})),r},e.exports=v},49374:(e,t,r)=>{"use strict";var n=r(17331),i=r(68078),s=r(94039).escapeFacetValue,a=r(14853),c=r(60185),u=r(90116),o=r(49803),h=r(96394),f=r(17775),l=r(23076),m=r(24336);function d(e,t,r){"function"==typeof e.addAlgoliaAgent&&e.addAlgoliaAgent("JS Helper ("+m+")"),this.setClient(e);var n=r||{};n.index=t,this.state=f.make(n),this.lastResults=null,this._queryId=0,this._lastQueryIdReceived=-1,this.derivedHelpers=[],this._currentNbQueries=0}function p(e){if(e<0)throw new Error("Page requested below 0.");return this._change({state:this.state.setPage(e),isPageReset:!1}),this}function v(){return this.state.page}a(d,n),d.prototype.search=function(){return this._search({onlyWithDerivedHelpers:!1}),this},d.prototype.searchOnlyWithDerivedHelpers=function(){return this._search({onlyWithDerivedHelpers:!0}),this},d.prototype.getQuery=function(){var e=this.state;return h._getHitsSearchParams(e)},d.prototype.searchOnce=function(e,t){var r=e?this.state.setQueryParameters(e):this.state,n=h._getQueries(r.index,r),i=this;if(this._currentNbQueries++,this.emit("searchOnce",{state:r}),!t)return this.client.search(n).then((function(e){return i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),{content:new l(r,e.results),state:r,_originalResponse:e}}),(function(e){throw i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),e}));this.client.search(n).then((function(e){i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),t(null,new l(r,e.results),r)})).catch((function(e){i._currentNbQueries--,0===i._currentNbQueries&&i.emit("searchQueueEmpty"),t(e,null,r)}))},d.prototype.findAnswers=function(e){console.warn("[algoliasearch-helper] answers is no longer supported");var t=this.state,r=this.derivedHelpers[0];if(!r)return Promise.resolve([]);var n=r.getModifiedState(t),i=c({attributesForPrediction:e.attributesForPrediction,nbHits:e.nbHits},{params:o(h._getHitsSearchParams(n),["attributesToSnippet","hitsPerPage","restrictSearchableAttributes","snippetEllipsisText"])}),s="search for answers was called, but this client does not have a function client.initIndex(index).findAnswers";if("function"!=typeof this.client.initIndex)throw new Error(s);var a=this.client.initIndex(n.index);if("function"!=typeof a.findAnswers)throw new Error(s);return a.findAnswers(n.query,e.queryLanguages,i)},d.prototype.searchForFacetValues=function(e,t,r,n){var i="function"==typeof this.client.searchForFacetValues,a="function"==typeof this.client.initIndex;if(!i&&!a&&"function"!=typeof this.client.search)throw new Error("search for facet values (searchable) was called, but this client does not have a function client.searchForFacetValues or client.initIndex(index).searchForFacetValues");var c=this.state.setQueryParameters(n||{}),u=c.isDisjunctiveFacet(e),o=h.getSearchForFacetQuery(e,t,r,c);this._currentNbQueries++;var f,l=this;return i?f=this.client.searchForFacetValues([{indexName:c.index,params:o}]):a?f=this.client.initIndex(c.index).searchForFacetValues(o):(delete o.facetName,f=this.client.search([{type:"facet",facet:e,indexName:c.index,params:o}]).then((function(e){return e.results[0]}))),this.emit("searchForFacetValues",{state:c,facet:e,query:t}),f.then((function(t){return l._currentNbQueries--,0===l._currentNbQueries&&l.emit("searchQueueEmpty"),(t=Array.isArray(t)?t[0]:t).facetHits.forEach((function(t){t.escapedValue=s(t.value),t.isRefined=u?c.isDisjunctiveFacetRefined(e,t.escapedValue):c.isFacetRefined(e,t.escapedValue)})),t}),(function(e){throw l._currentNbQueries--,0===l._currentNbQueries&&l.emit("searchQueueEmpty"),e}))},d.prototype.setQuery=function(e){return this._change({state:this.state.resetPage().setQuery(e),isPageReset:!0}),this},d.prototype.clearRefinements=function(e){return this._change({state:this.state.resetPage().clearRefinements(e),isPageReset:!0}),this},d.prototype.clearTags=function(){return this._change({state:this.state.resetPage().clearTags(),isPageReset:!0}),this},d.prototype.addDisjunctiveFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addDisjunctiveFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addDisjunctiveRefine=function(){return this.addDisjunctiveFacetRefinement.apply(this,arguments)},d.prototype.addHierarchicalFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addHierarchicalFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addNumericRefinement=function(e,t,r){return this._change({state:this.state.resetPage().addNumericRefinement(e,t,r),isPageReset:!0}),this},d.prototype.addFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().addFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.addRefine=function(){return this.addFacetRefinement.apply(this,arguments)},d.prototype.addFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().addExcludeRefinement(e,t),isPageReset:!0}),this},d.prototype.addExclude=function(){return this.addFacetExclusion.apply(this,arguments)},d.prototype.addTag=function(e){return this._change({state:this.state.resetPage().addTagRefinement(e),isPageReset:!0}),this},d.prototype.removeNumericRefinement=function(e,t,r){return this._change({state:this.state.resetPage().removeNumericRefinement(e,t,r),isPageReset:!0}),this},d.prototype.removeDisjunctiveFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().removeDisjunctiveFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.removeDisjunctiveRefine=function(){return this.removeDisjunctiveFacetRefinement.apply(this,arguments)},d.prototype.removeHierarchicalFacetRefinement=function(e){return this._change({state:this.state.resetPage().removeHierarchicalFacetRefinement(e),isPageReset:!0}),this},d.prototype.removeFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().removeFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.removeRefine=function(){return this.removeFacetRefinement.apply(this,arguments)},d.prototype.removeFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().removeExcludeRefinement(e,t),isPageReset:!0}),this},d.prototype.removeExclude=function(){return this.removeFacetExclusion.apply(this,arguments)},d.prototype.removeTag=function(e){return this._change({state:this.state.resetPage().removeTagRefinement(e),isPageReset:!0}),this},d.prototype.toggleFacetExclusion=function(e,t){return this._change({state:this.state.resetPage().toggleExcludeFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.toggleExclude=function(){return this.toggleFacetExclusion.apply(this,arguments)},d.prototype.toggleRefinement=function(e,t){return this.toggleFacetRefinement(e,t)},d.prototype.toggleFacetRefinement=function(e,t){return this._change({state:this.state.resetPage().toggleFacetRefinement(e,t),isPageReset:!0}),this},d.prototype.toggleRefine=function(){return this.toggleFacetRefinement.apply(this,arguments)},d.prototype.toggleTag=function(e){return this._change({state:this.state.resetPage().toggleTagRefinement(e),isPageReset:!0}),this},d.prototype.nextPage=function(){var e=this.state.page||0;return this.setPage(e+1)},d.prototype.previousPage=function(){var e=this.state.page||0;return this.setPage(e-1)},d.prototype.setCurrentPage=p,d.prototype.setPage=p,d.prototype.setIndex=function(e){return this._change({state:this.state.resetPage().setIndex(e),isPageReset:!0}),this},d.prototype.setQueryParameter=function(e,t){return this._change({state:this.state.resetPage().setQueryParameter(e,t),isPageReset:!0}),this},d.prototype.setState=function(e){return this._change({state:f.make(e),isPageReset:!1}),this},d.prototype.overrideStateWithoutTriggeringChangeEvent=function(e){return this.state=new f(e),this},d.prototype.hasRefinements=function(e){return!!u(this.state.getNumericRefinements(e))||(this.state.isConjunctiveFacet(e)?this.state.isFacetRefined(e):this.state.isDisjunctiveFacet(e)?this.state.isDisjunctiveFacetRefined(e):!!this.state.isHierarchicalFacet(e)&&this.state.isHierarchicalFacetRefined(e))},d.prototype.isExcluded=function(e,t){return this.state.isExcludeRefined(e,t)},d.prototype.isDisjunctiveRefined=function(e,t){return this.state.isDisjunctiveFacetRefined(e,t)},d.prototype.hasTag=function(e){return this.state.isTagRefined(e)},d.prototype.isTagRefined=function(){return this.hasTagRefinements.apply(this,arguments)},d.prototype.getIndex=function(){return this.state.index},d.prototype.getCurrentPage=v,d.prototype.getPage=v,d.prototype.getTags=function(){return this.state.tagRefinements},d.prototype.getRefinements=function(e){var t=[];if(this.state.isConjunctiveFacet(e))this.state.getConjunctiveRefinements(e).forEach((function(e){t.push({value:e,type:"conjunctive"})})),this.state.getExcludeRefinements(e).forEach((function(e){t.push({value:e,type:"exclude"})}));else if(this.state.isDisjunctiveFacet(e)){this.state.getDisjunctiveRefinements(e).forEach((function(e){t.push({value:e,type:"disjunctive"})}))}var r=this.state.getNumericRefinements(e);return Object.keys(r).forEach((function(e){var n=r[e];t.push({value:n,operator:e,type:"numeric"})})),t},d.prototype.getNumericRefinement=function(e,t){return this.state.getNumericRefinement(e,t)},d.prototype.getHierarchicalFacetBreadcrumb=function(e){return this.state.getHierarchicalFacetBreadcrumb(e)},d.prototype._search=function(e){var t=this.state,r=[],n=[];e.onlyWithDerivedHelpers||(n=h._getQueries(t.index,t),r.push({state:t,queriesCount:n.length,helper:this}),this.emit("search",{state:t,results:this.lastResults}));var i=this.derivedHelpers.map((function(e){var n=e.getModifiedState(t),i=n.index?h._getQueries(n.index,n):[];return r.push({state:n,queriesCount:i.length,helper:e}),e.emit("search",{state:n,results:e.lastResults}),i})),s=Array.prototype.concat.apply(n,i),a=this._queryId++;if(this._currentNbQueries++,!s.length)return Promise.resolve({results:[]}).then(this._dispatchAlgoliaResponse.bind(this,r,a));try{this.client.search(s).then(this._dispatchAlgoliaResponse.bind(this,r,a)).catch(this._dispatchAlgoliaError.bind(this,a))}catch(c){this.emit("error",{error:c})}},d.prototype._dispatchAlgoliaResponse=function(e,t,r){if(!(t<this._lastQueryIdReceived)){this._currentNbQueries-=t-this._lastQueryIdReceived,this._lastQueryIdReceived=t,0===this._currentNbQueries&&this.emit("searchQueueEmpty");var n=r.results.slice();e.forEach((function(e){var t=e.state,r=e.queriesCount,i=e.helper,s=n.splice(0,r);t.index?(i.lastResults=new l(t,s),i.emit("result",{results:i.lastResults,state:t})):i.emit("result",{results:null,state:t})}))}},d.prototype._dispatchAlgoliaError=function(e,t){e<this._lastQueryIdReceived||(this._currentNbQueries-=e-this._lastQueryIdReceived,this._lastQueryIdReceived=e,this.emit("error",{error:t}),0===this._currentNbQueries&&this.emit("searchQueueEmpty"))},d.prototype.containsRefinement=function(e,t,r,n){return e||0!==t.length||0!==r.length||0!==n.length},d.prototype._hasDisjunctiveRefinements=function(e){return this.state.disjunctiveRefinements[e]&&this.state.disjunctiveRefinements[e].length>0},d.prototype._change=function(e){var t=e.state,r=e.isPageReset;t!==this.state&&(this.state=t,this.emit("change",{state:this.state,results:this.lastResults,isPageReset:r}))},d.prototype.clearCache=function(){return this.client.clearCache&&this.client.clearCache(),this},d.prototype.setClient=function(e){return this.client===e||("function"==typeof e.addAlgoliaAgent&&e.addAlgoliaAgent("JS Helper ("+m+")"),this.client=e),this},d.prototype.getClient=function(){return this.client},d.prototype.derive=function(e){var t=new i(this,e);return this.derivedHelpers.push(t),t},d.prototype.detachDerivedHelper=function(e){var t=this.derivedHelpers.indexOf(e);if(-1===t)throw new Error("Derived helper already detached");this.derivedHelpers.splice(t,1)},d.prototype.hasPendingRequests=function(){return this._currentNbQueries>0},e.exports=d},74587:e=>{"use strict";e.exports=function(e){return Array.isArray(e)?e.filter(Boolean):[]}},52344:e=>{"use strict";e.exports=function(){return Array.prototype.slice.call(arguments).reduceRight((function(e,t){return Object.keys(Object(t)).forEach((function(r){void 0!==t[r]&&(void 0!==e[r]&&delete e[r],e[r]=t[r])})),e}),{})}},94039:e=>{"use strict";e.exports={escapeFacetValue:function(e){return"string"!=typeof e?e:String(e).replace(/^-/,"\\-")},unescapeFacetValue:function(e){return"string"!=typeof e?e:e.replace(/^\\-/,"-")}}},7888:e=>{"use strict";e.exports=function(e,t){if(Array.isArray(e))for(var r=0;r<e.length;r++)if(t(e[r]))return e[r]}},69725:e=>{"use strict";e.exports=function(e,t){if(!Array.isArray(e))return-1;for(var r=0;r<e.length;r++)if(t(e[r]))return r;return-1}},82293:(e,t,r)=>{"use strict";var n=r(7888);e.exports=function(e,t){var r=(t||[]).map((function(e){return e.split(":")}));return e.reduce((function(e,t){var i=t.split(":"),s=n(r,(function(e){return e[0]===i[0]}));return i.length>1||!s?(e[0].push(i[0]),e[1].push(i[1]),e):(e[0].push(s[0]),e[1].push(s[1]),e)}),[[],[]])}},14853:e=>{"use strict";e.exports=function(e,t){e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}},22686:e=>{"use strict";e.exports=function(e,t){return e.filter((function(r,n){return t.indexOf(r)>-1&&e.indexOf(r)===n}))}},60185:e=>{"use strict";function t(e){return"function"==typeof e||Array.isArray(e)||"[object Object]"===Object.prototype.toString.call(e)}function r(e,n){if(e===n)return e;for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)&&"__proto__"!==i&&"constructor"!==i){var s=n[i],a=e[i];void 0!==a&&void 0===s||(t(a)&&t(s)?e[i]=r(a,s):e[i]="object"==typeof(c=s)&&null!==c?r(Array.isArray(c)?[]:{},c):c)}var c;return e}e.exports=function(e){t(e)||(e={});for(var n=1,i=arguments.length;n<i;n++){var s=arguments[n];t(s)&&r(e,s)}return e}},90116:e=>{"use strict";e.exports=function(e){return e&&Object.keys(e).length>0}},49803:e=>{"use strict";e.exports=function(e,t){if(null===e)return{};var r,n,i={},s=Object.keys(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||(i[r]=e[r]);return i}},42148:e=>{"use strict";function t(e,t){if(e!==t){var r=void 0!==e,n=null===e,i=void 0!==t,s=null===t;if(!s&&e>t||n&&i||!r)return 1;if(!n&&e<t||s&&r||!i)return-1}return 0}e.exports=function(e,r,n){if(!Array.isArray(e))return[];Array.isArray(n)||(n=[]);var i=e.map((function(e,t){return{criteria:r.map((function(t){return e[t]})),index:t,value:e}}));return i.sort((function(e,r){for(var i=-1;++i<e.criteria.length;){var s=t(e.criteria[i],r.criteria[i]);if(s)return i>=n.length?s:"desc"===n[i]?-s:s}return e.index-r.index})),i.map((function(e){return e.value}))}},28023:e=>{"use strict";e.exports=function e(t){if("number"==typeof t)return t;if("string"==typeof t)return parseFloat(t);if(Array.isArray(t))return t.map(e);throw new Error("The value should be a number, a parsable string or an array of those.")}},96394:(e,t,r)=>{"use strict";var n=r(60185);function i(e){return Object.keys(e).sort().reduce((function(t,r){return t[r]=e[r],t}),{})}var s={_getQueries:function(e,t){var r=[];return r.push({indexName:e,params:s._getHitsSearchParams(t)}),t.getRefinedDisjunctiveFacets().forEach((function(n){r.push({indexName:e,params:s._getDisjunctiveFacetSearchParams(t,n)})})),t.getRefinedHierarchicalFacets().forEach((function(n){var i=t.getHierarchicalFacetByName(n),a=t.getHierarchicalRefinement(n),c=t._getHierarchicalFacetSeparator(i);if(a.length>0&&a[0].split(c).length>1){var u=a[0].split(c).slice(0,-1).reduce((function(e,t,r){return e.concat({attribute:i.attributes[r],value:0===r?t:[e[e.length-1].value,t].join(c)})}),[]);u.forEach((function(n,a){var c=s._getDisjunctiveFacetSearchParams(t,n.attribute,0===a);function o(e){return i.attributes.some((function(t){return t===e.split(":")[0]}))}var h=(c.facetFilters||[]).reduce((function(e,t){if(Array.isArray(t)){var r=t.filter((function(e){return!o(e)}));r.length>0&&e.push(r)}return"string"!=typeof t||o(t)||e.push(t),e}),[]),f=u[a-1];c.facetFilters=a>0?h.concat(f.attribute+":"+f.value):h.length>0?h:void 0,r.push({indexName:e,params:c})}))}})),r},_getHitsSearchParams:function(e){var t=e.facets.concat(e.disjunctiveFacets).concat(s._getHitsHierarchicalFacetsAttributes(e)).sort(),r=s._getFacetFilters(e),a=s._getNumericFilters(e),c=s._getTagFilters(e),u={facets:t.indexOf("*")>-1?["*"]:t,tagFilters:c};return r.length>0&&(u.facetFilters=r),a.length>0&&(u.numericFilters=a),i(n({},e.getQueryParams(),u))},_getDisjunctiveFacetSearchParams:function(e,t,r){var a=s._getFacetFilters(e,t,r),c=s._getNumericFilters(e,t),u=s._getTagFilters(e),o={hitsPerPage:0,page:0,analytics:!1,clickAnalytics:!1};u.length>0&&(o.tagFilters=u);var h=e.getHierarchicalFacetByName(t);return o.facets=h?s._getDisjunctiveHierarchicalFacetAttribute(e,h,r):t,c.length>0&&(o.numericFilters=c),a.length>0&&(o.facetFilters=a),i(n({},e.getQueryParams(),o))},_getNumericFilters:function(e,t){if(e.numericFilters)return e.numericFilters;var r=[];return Object.keys(e.numericRefinements).forEach((function(n){var i=e.numericRefinements[n]||{};Object.keys(i).forEach((function(e){var s=i[e]||[];t!==n&&s.forEach((function(t){if(Array.isArray(t)){var i=t.map((function(t){return n+e+t}));r.push(i)}else r.push(n+e+t)}))}))})),r},_getTagFilters:function(e){return e.tagFilters?e.tagFilters:e.tagRefinements.join(",")},_getFacetFilters:function(e,t,r){var n=[],i=e.facetsRefinements||{};Object.keys(i).sort().forEach((function(e){(i[e]||[]).sort().forEach((function(t){n.push(e+":"+t)}))}));var s=e.facetsExcludes||{};Object.keys(s).sort().forEach((function(e){(s[e]||[]).sort().forEach((function(t){n.push(e+":-"+t)}))}));var a=e.disjunctiveFacetsRefinements||{};Object.keys(a).sort().forEach((function(e){var r=a[e]||[];if(e!==t&&r&&0!==r.length){var i=[];r.sort().forEach((function(t){i.push(e+":"+t)})),n.push(i)}}));var c=e.hierarchicalFacetsRefinements||{};return Object.keys(c).sort().forEach((function(i){var s=(c[i]||[])[0];if(void 0!==s){var a,u,o=e.getHierarchicalFacetByName(i),h=e._getHierarchicalFacetSeparator(o),f=e._getHierarchicalRootPath(o);if(t===i){if(-1===s.indexOf(h)||!f&&!0===r||f&&f.split(h).length===s.split(h).length)return;f?(u=f.split(h).length-1,s=f):(u=s.split(h).length-2,s=s.slice(0,s.lastIndexOf(h))),a=o.attributes[u]}else u=s.split(h).length-1,a=o.attributes[u];a&&n.push([a+":"+s])}})),n},_getHitsHierarchicalFacetsAttributes:function(e){return e.hierarchicalFacets.reduce((function(t,r){var n=e.getHierarchicalRefinement(r.name)[0];if(!n)return t.push(r.attributes[0]),t;var i=e._getHierarchicalFacetSeparator(r),s=n.split(i).length,a=r.attributes.slice(0,s+1);return t.concat(a)}),[])},_getDisjunctiveHierarchicalFacetAttribute:function(e,t,r){var n=e._getHierarchicalFacetSeparator(t);if(!0===r){var i=e._getHierarchicalRootPath(t),s=0;return i&&(s=i.split(n).length),[t.attributes[s]]}var a=(e.getHierarchicalRefinement(t.name)[0]||"").split(n).length-1;return t.attributes.slice(0,a+1)},getSearchForFacetQuery:function(e,t,r,a){var c=a.isDisjunctiveFacet(e)?a.clearRefinements(e):a,u={facetQuery:t,facetName:e};return"number"==typeof r&&(u.maxFacetHits=r),i(n({},s._getHitsSearchParams(c),u))}};e.exports=s},46801:e=>{"use strict";e.exports=function(e){return null!==e&&/^[a-zA-Z0-9_-]{1,64}$/.test(e)}},24336:e=>{"use strict";e.exports="3.15.0"},70290:function(e){e.exports=function(){"use strict";function e(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function t(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function r(r){for(var n=1;n<arguments.length;n++){var i=null!=arguments[n]?arguments[n]:{};n%2?t(Object(i),!0).forEach((function(t){e(r,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(r,Object.getOwnPropertyDescriptors(i)):t(Object(i)).forEach((function(e){Object.defineProperty(r,e,Object.getOwnPropertyDescriptor(i,e))}))}return r}function n(e,t){if(null==e)return{};var r,n,i=function(e,t){if(null==e)return{};var r,n,i={},s=Object.keys(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||(i[r]=e[r]);return i}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n<s.length;n++)r=s[n],t.indexOf(r)>=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(i[r]=e[r])}return i}function i(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e)){var r=[],n=!0,i=!1,s=void 0;try{for(var a,c=e[Symbol.iterator]();!(n=(a=c.next()).done)&&(r.push(a.value),!t||r.length!==t);n=!0);}catch(e){i=!0,s=e}finally{try{n||null==c.return||c.return()}finally{if(i)throw s}}return r}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function s(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}function a(e){var t,r="algoliasearch-client-js-".concat(e.key),n=function(){return void 0===t&&(t=e.localStorage||window.localStorage),t},s=function(){return JSON.parse(n().getItem(r)||"{}")},a=function(e){n().setItem(r,JSON.stringify(e))},c=function(){var t=e.timeToLive?1e3*e.timeToLive:null,r=s(),n=Object.fromEntries(Object.entries(r).filter((function(e){return void 0!==i(e,2)[1].timestamp})));if(a(n),t){var c=Object.fromEntries(Object.entries(n).filter((function(e){var r=i(e,2)[1],n=(new Date).getTime();return!(r.timestamp+t<n)})));a(c)}};return{get:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return Promise.resolve().then((function(){c();var t=JSON.stringify(e);return s()[t]})).then((function(e){return Promise.all([e?e.value:t(),void 0!==e])})).then((function(e){var t=i(e,2),n=t[0],s=t[1];return Promise.all([n,s||r.miss(n)])})).then((function(e){return i(e,1)[0]}))},set:function(e,t){return Promise.resolve().then((function(){var i=s();return i[JSON.stringify(e)]={timestamp:(new Date).getTime(),value:t},n().setItem(r,JSON.stringify(i)),t}))},delete:function(e){return Promise.resolve().then((function(){var t=s();delete t[JSON.stringify(e)],n().setItem(r,JSON.stringify(t))}))},clear:function(){return Promise.resolve().then((function(){n().removeItem(r)}))}}}function c(e){var t=s(e.caches),r=t.shift();return void 0===r?{get:function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return t().then((function(e){return Promise.all([e,r.miss(e)])})).then((function(e){return i(e,1)[0]}))},set:function(e,t){return Promise.resolve(t)},delete:function(e){return Promise.resolve()},clear:function(){return Promise.resolve()}}:{get:function(e,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}};return r.get(e,n,i).catch((function(){return c({caches:t}).get(e,n,i)}))},set:function(e,n){return r.set(e,n).catch((function(){return c({caches:t}).set(e,n)}))},delete:function(e){return r.delete(e).catch((function(){return c({caches:t}).delete(e)}))},clear:function(){return r.clear().catch((function(){return c({caches:t}).clear()}))}}}function u(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{serializable:!0},t={};return{get:function(r,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{miss:function(){return Promise.resolve()}},s=JSON.stringify(r);if(s in t)return Promise.resolve(e.serializable?JSON.parse(t[s]):t[s]);var a=n(),c=i&&i.miss||function(){return Promise.resolve()};return a.then((function(e){return c(e)})).then((function(){return a}))},set:function(r,n){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(n):n,Promise.resolve(n)},delete:function(e){return delete t[JSON.stringify(e)],Promise.resolve()},clear:function(){return t={},Promise.resolve()}}}function o(e){for(var t=e.length-1;t>0;t--){var r=Math.floor(Math.random()*(t+1)),n=e[t];e[t]=e[r],e[r]=n}return e}function h(e,t){return t?(Object.keys(t).forEach((function(r){e[r]=t[r](e)})),e):e}function f(e){for(var t=arguments.length,r=new Array(t>1?t-1:0),n=1;n<t;n++)r[n-1]=arguments[n];var i=0;return e.replace(/%s/g,(function(){return encodeURIComponent(r[i++])}))}var l={WithinQueryParameters:0,WithinHeaders:1};function m(e,t){var r=e||{},n=r.data||{};return Object.keys(r).forEach((function(e){-1===["timeout","headers","queryParameters","data","cacheable"].indexOf(e)&&(n[e]=r[e])})),{data:Object.entries(n).length>0?n:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var d={Read:1,Write:2,Any:3},p=1,v=2,g=3;function y(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:p;return r(r({},e),{},{status:t,lastUpdate:Date.now()})}function R(e){return"string"==typeof e?{protocol:"https",url:e,accept:d.Any}:{protocol:e.protocol||"https",url:e.url,accept:e.accept||d.Any}}var F="GET",b="POST";function j(e,t){return Promise.all(t.map((function(t){return e.get(t,(function(){return Promise.resolve(y(t))}))}))).then((function(e){var r=e.filter((function(e){return function(e){return e.status===p||Date.now()-e.lastUpdate>12e4}(e)})),n=e.filter((function(e){return function(e){return e.status===g&&Date.now()-e.lastUpdate<=12e4}(e)})),i=[].concat(s(r),s(n));return{getTimeout:function(e,t){return(0===n.length&&0===e?1:n.length+3+e)*t},statelessHosts:i.length>0?i.map((function(e){return R(e)})):t}}))}function P(e,t,n,i){var a=[],c=function(e,t){if(e.method!==F&&(void 0!==e.data||void 0!==t.data)){var n=Array.isArray(e.data)?e.data:r(r({},e.data),t.data);return JSON.stringify(n)}}(n,i),u=function(e,t){var n=r(r({},e.headers),t.headers),i={};return Object.keys(n).forEach((function(e){var t=n[e];i[e.toLowerCase()]=t})),i}(e,i),o=n.method,h=n.method!==F?{}:r(r({},n.data),i.data),f=r(r(r({"x-algolia-agent":e.userAgent.value},e.queryParameters),h),i.queryParameters),l=0,m=function t(r,s){var h=r.pop();if(void 0===h)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:O(a)};var m={data:c,headers:u,method:o,url:_(h,n.path,f),connectTimeout:s(l,e.timeouts.connect),responseTimeout:s(l,i.timeout)},d=function(e){var t={request:m,response:e,host:h,triesLeft:r.length};return a.push(t),t},p={onSuccess:function(e){return function(e){try{return JSON.parse(e.content)}catch(t){throw function(e,t){return{name:"DeserializationError",message:e,response:t}}(t.message,e)}}(e)},onRetry:function(n){var i=d(n);return n.isTimedOut&&l++,Promise.all([e.logger.info("Retryable failure",w(i)),e.hostsCache.set(h,y(h,n.isTimedOut?g:v))]).then((function(){return t(r,s)}))},onFail:function(e){throw d(e),function(e,t){var r=e.content,n=e.status,i=r;try{i=JSON.parse(r).message}catch(e){}return function(e,t,r){return{name:"ApiError",message:e,status:t,transporterStackTrace:r}}(i,n,t)}(e,O(a))}};return e.requester.send(m).then((function(e){return function(e,t){return function(e){var t=e.status;return e.isTimedOut||function(e){var t=e.isTimedOut,r=e.status;return!t&&0==~~r}(e)||2!=~~(t/100)&&4!=~~(t/100)}(e)?t.onRetry(e):2==~~(e.status/100)?t.onSuccess(e):t.onFail(e)}(e,p)}))};return j(e.hostsCache,t).then((function(e){return m(s(e.statelessHosts).reverse(),e.getTimeout)}))}function x(e){var t={value:"Algolia for JavaScript (".concat(e,")"),add:function(e){var r="; ".concat(e.segment).concat(void 0!==e.version?" (".concat(e.version,")"):"");return-1===t.value.indexOf(r)&&(t.value="".concat(t.value).concat(r)),t}};return t}function _(e,t,r){var n=E(r),i="".concat(e.protocol,"://").concat(e.url,"/").concat("/"===t.charAt(0)?t.substr(1):t);return n.length&&(i+="?".concat(n)),i}function E(e){return Object.keys(e).map((function(t){return f("%s=%s",t,(r=e[t],"[object Object]"===Object.prototype.toString.call(r)||"[object Array]"===Object.prototype.toString.call(r)?JSON.stringify(e[t]):e[t]));var r})).join("&")}function O(e){return e.map((function(e){return w(e)}))}function w(e){var t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return r(r({},e),{},{request:r(r({},e.request),{},{headers:r(r({},e.request.headers),t)})})}var A=function(e){var t=e.appId,n=function(e,t,r){var n={"x-algolia-api-key":r,"x-algolia-application-id":t};return{headers:function(){return e===l.WithinHeaders?n:{}},queryParameters:function(){return e===l.WithinQueryParameters?n:{}}}}(void 0!==e.authMode?e.authMode:l.WithinHeaders,t,e.apiKey),s=function(e){var t=e.hostsCache,r=e.logger,n=e.requester,s=e.requestsCache,a=e.responsesCache,c=e.timeouts,u=e.userAgent,o=e.hosts,h=e.queryParameters,f={hostsCache:t,logger:r,requester:n,requestsCache:s,responsesCache:a,timeouts:c,userAgent:u,headers:e.headers,queryParameters:h,hosts:o.map((function(e){return R(e)})),read:function(e,t){var r=m(t,f.timeouts.read),n=function(){return P(f,f.hosts.filter((function(e){return 0!=(e.accept&d.Read)})),e,r)};if(!0!==(void 0!==r.cacheable?r.cacheable:e.cacheable))return n();var s={request:e,mappedRequestOptions:r,transporter:{queryParameters:f.queryParameters,headers:f.headers}};return f.responsesCache.get(s,(function(){return f.requestsCache.get(s,(function(){return f.requestsCache.set(s,n()).then((function(e){return Promise.all([f.requestsCache.delete(s),e])}),(function(e){return Promise.all([f.requestsCache.delete(s),Promise.reject(e)])})).then((function(e){var t=i(e,2);return t[0],t[1]}))}))}),{miss:function(e){return f.responsesCache.set(s,e)}})},write:function(e,t){return P(f,f.hosts.filter((function(e){return 0!=(e.accept&d.Write)})),e,m(t,f.timeouts.write))}};return f}(r(r({hosts:[{url:"".concat(t,"-dsn.algolia.net"),accept:d.Read},{url:"".concat(t,".algolia.net"),accept:d.Write}].concat(o([{url:"".concat(t,"-1.algolianet.com")},{url:"".concat(t,"-2.algolianet.com")},{url:"".concat(t,"-3.algolianet.com")}]))},e),{},{headers:r(r(r({},n.headers()),{"content-type":"application/x-www-form-urlencoded"}),e.headers),queryParameters:r(r({},n.queryParameters()),e.queryParameters)}));return h({transporter:s,appId:t,addAlgoliaAgent:function(e,t){s.userAgent.add({segment:e,version:t})},clearCache:function(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then((function(){}))}},e.methods)},N=function(e){return function(t,r){return t.method===F?e.transporter.read(t,r):e.transporter.write(t,r)}},H=function(e){return function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return h({transporter:e.transporter,appId:e.appId,indexName:t},r.methods)}},S=function(e){return function(t,n){var i=t.map((function(e){return r(r({},e),{},{params:E(e.params||{})})}));return e.transporter.read({method:b,path:"1/indexes/*/queries",data:{requests:i},cacheable:!0},n)}},T=function(e){return function(t,i){return Promise.all(t.map((function(t){var s=t.params,a=s.facetName,c=s.facetQuery,u=n(s,["facetName","facetQuery"]);return H(e)(t.indexName,{methods:{searchForFacetValues:I}}).searchForFacetValues(a,c,r(r({},i),u))})))}},Q=function(e){return function(t,r,n){return e.transporter.read({method:b,path:f("1/answers/%s/prediction",e.indexName),data:{query:t,queryLanguages:r},cacheable:!0},n)}},C=function(e){return function(t,r){return e.transporter.read({method:b,path:f("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},r)}},I=function(e){return function(t,r,n){return e.transporter.read({method:b,path:f("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:r},cacheable:!0},n)}},D=1,k=2,q=3;function V(e,t,n){var i,s={appId:e,apiKey:t,timeouts:{connect:1,read:2,write:30},requester:{send:function(e){return new Promise((function(t){var r=new XMLHttpRequest;r.open(e.method,e.url,!0),Object.keys(e.headers).forEach((function(t){return r.setRequestHeader(t,e.headers[t])}));var n,i=function(e,n){return setTimeout((function(){r.abort(),t({status:0,content:n,isTimedOut:!0})}),1e3*e)},s=i(e.connectTimeout,"Connection timeout");r.onreadystatechange=function(){r.readyState>r.OPENED&&void 0===n&&(clearTimeout(s),n=i(e.responseTimeout,"Socket timeout"))},r.onerror=function(){0===r.status&&(clearTimeout(s),clearTimeout(n),t({content:r.responseText||"Network request failed",status:r.status,isTimedOut:!1}))},r.onload=function(){clearTimeout(s),clearTimeout(n),t({content:r.responseText,status:r.status,isTimedOut:!1})},r.send(e.data)}))}},logger:(i=q,{debug:function(e,t){return D>=i&&console.debug(e,t),Promise.resolve()},info:function(e,t){return k>=i&&console.info(e,t),Promise.resolve()},error:function(e,t){return console.error(e,t),Promise.resolve()}}),responsesCache:u(),requestsCache:u({serializable:!1}),hostsCache:c({caches:[a({key:"".concat("4.20.0","-").concat(e)}),u()]}),userAgent:x("4.20.0").add({segment:"Browser",version:"lite"}),authMode:l.WithinQueryParameters};return A(r(r(r({},s),n),{},{methods:{search:S,searchForFacetValues:T,multipleQueries:S,multipleSearchForFacetValues:T,customRequest:N,initIndex:function(e){return function(t){return H(e)(t,{methods:{search:C,searchForFacetValues:I,findAnswers:Q}})}}}}))}return V.version="4.20.0",V}()},88824:(e,t,r)=>{"use strict";r.d(t,{c:()=>o});var n=r(67294),i=r(52263);const s=["zero","one","two","few","many","other"];function a(e){return s.filter((t=>e.includes(t)))}const c={locale:"en",pluralForms:a(["one","other"]),select:e=>1===e?"one":"other"};function u(){const{i18n:{currentLocale:e}}=(0,i.Z)();return(0,n.useMemo)((()=>{try{return function(e){const t=new Intl.PluralRules(e);return{locale:e,pluralForms:a(t.resolvedOptions().pluralCategories),select:e=>t.select(e)}}(e)}catch(t){return console.error(`Failed to use Intl.PluralRules for locale "${e}".\nDocusaurus will fallback to the default (English) implementation.\nError: ${t.message}\n`),c}}),[e])}function o(){const e=u();return{selectMessage:(t,r)=>function(e,t,r){const n=e.split("|");if(1===n.length)return n[0];n.length>r.pluralForms.length&&console.error(`For locale=${r.locale}, a maximum of ${r.pluralForms.length} plural forms are expected (${r.pluralForms.join(",")}), but the message contains ${n.length}: ${e}`);const i=r.select(t),s=r.pluralForms.indexOf(i);return n[Math.min(s,n.length-1)]}(r,t,e)}}},48852:(e,t,r)=>{"use strict";r.r(t),r.d(t,{default:()=>A});var n=r(67294);function i(e){var t,r,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var s=e.length;for(t=0;t<s;t++)e[t]&&(r=i(e[t]))&&(n&&(n+=" "),n+=r)}else for(r in e)e[r]&&(n&&(n+=" "),n+=r);return n}const s=function(){for(var e,t,r=0,n="",s=arguments.length;r<s;r++)(e=arguments[r])&&(t=i(e))&&(n&&(n+=" "),n+=t);return n};var a=r(8131),c=r.n(a),u=r(70290),o=r.n(u),h=r(10412),f=r(35742),l=r(33692),m=r(80143),d=r(88824),p=r(66177),v=r(902),g=r(71667),y=r(82128),R=r(95999),F=r(52263),b=r(6278),j=r(239),P=r(7372),x=r(92503);const _={searchQueryInput:"searchQueryInput_u2C7",searchVersionInput:"searchVersionInput_m0Ui",searchResultsColumn:"searchResultsColumn_JPFH",algoliaLogo:"algoliaLogo_rT1R",algoliaLogoPathFill:"algoliaLogoPathFill_WdUC",searchResultItem:"searchResultItem_Tv2o",searchResultItemHeading:"searchResultItemHeading_KbCB",searchResultItemPath:"searchResultItemPath_lhe1",searchResultItemSummary:"searchResultItemSummary_AEaO",searchQueryColumn:"searchQueryColumn_RTkw",searchVersionColumn:"searchVersionColumn_ypXd",searchLogoColumn:"searchLogoColumn_rJIA",loadingSpinner:"loadingSpinner_XVxU","loading-spin":"loading-spin_vzvp",loader:"loader_vvXV"};var E=r(85893);function O(e){let{docsSearchVersionsHelpers:t}=e;const r=Object.entries(t.allDocsData).filter((e=>{let[,t]=e;return t.versions.length>1}));return(0,E.jsx)("div",{className:s("col","col--3","padding-left--none",_.searchVersionColumn),children:r.map((e=>{let[n,i]=e;const s=r.length>1?`${n}: `:"";return(0,E.jsx)("select",{onChange:e=>t.setSearchVersion(n,e.target.value),defaultValue:t.searchVersions[n],className:_.searchVersionInput,children:i.versions.map(((e,t)=>(0,E.jsx)("option",{label:`${s}${e.label}`,value:e.name},t)))},n)}))})}function w(){const{i18n:{currentLocale:e}}=(0,F.Z)(),{algolia:{appId:t,apiKey:r,indexName:i}}=(0,b.L)(),a=(0,j.l)(),u=function(){const{selectMessage:e}=(0,d.c)();return t=>e(t,(0,R.I)({id:"theme.SearchPage.documentsFound.plurals",description:'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',message:"One document found|{count} documents found"},{count:t}))}(),g=function(){const e=(0,m._r)(),[t,r]=(0,n.useState)((()=>Object.entries(e).reduce(((e,t)=>{let[r,n]=t;return{...e,[r]:n.versions[0].name}}),{}))),i=Object.values(e).some((e=>e.versions.length>1));return{allDocsData:e,versioningEnabled:i,searchVersions:t,setSearchVersion:(e,t)=>r((r=>({...r,[e]:t})))}}(),[w,A]=(0,p.K)(),N={items:[],query:null,totalResults:null,totalPages:null,lastPage:null,hasMore:null,loading:null},[H,S]=(0,n.useReducer)(((e,t)=>{switch(t.type){case"reset":return N;case"loading":return{...e,loading:!0};case"update":return w!==t.value.query?e:{...t.value,items:0===t.value.lastPage?t.value.items:e.items.concat(t.value.items)};case"advance":{const t=e.totalPages>e.lastPage+1;return{...e,lastPage:t?e.lastPage+1:e.lastPage,hasMore:t}}default:return e}}),N),T=o()(t,r),Q=c()(T,i,{hitsPerPage:15,advancedSyntax:!0,disjunctiveFacets:["language","docusaurus_tag"]});Q.on("result",(e=>{let{results:{query:t,hits:r,page:n,nbHits:i,nbPages:s}}=e;if(""===t||!Array.isArray(r))return void S({type:"reset"});const c=e=>e.replace(/algolia-docsearch-suggestion--highlight/g,"search-result-match"),u=r.map((e=>{let{url:t,_highlightResult:{hierarchy:r},_snippetResult:n={}}=e;const i=Object.keys(r).map((e=>c(r[e].value)));return{title:i.pop(),url:a(t),summary:n.content?`${c(n.content.value)}...`:"",breadcrumbs:i}}));S({type:"update",value:{items:u,query:t,totalResults:i,totalPages:s,lastPage:n,hasMore:s>n+1,loading:!1}})}));const[C,I]=(0,n.useState)(null),D=(0,n.useRef)(0),k=(0,n.useRef)(h.Z.canUseIntersectionObserver&&new IntersectionObserver((e=>{const{isIntersecting:t,boundingClientRect:{y:r}}=e[0];t&&D.current>r&&S({type:"advance"}),D.current=r}),{threshold:1})),q=()=>w?(0,R.I)({id:"theme.SearchPage.existingResultsTitle",message:'Search results for "{query}"',description:"The search page title for non-empty query"},{query:w}):(0,R.I)({id:"theme.SearchPage.emptyResultsTitle",message:"Search the documentation",description:"The search page title for empty query"}),V=(0,v.zX)((function(t){void 0===t&&(t=0),Q.addDisjunctiveFacetRefinement("docusaurus_tag","default"),Q.addDisjunctiveFacetRefinement("language",e),Object.entries(g.searchVersions).forEach((e=>{let[t,r]=e;Q.addDisjunctiveFacetRefinement("docusaurus_tag",`docs-${t}-${r}`)})),Q.setQuery(w).setPage(t).search()}));return(0,n.useEffect)((()=>{if(!C)return;const e=k.current;return e?(e.observe(C),()=>e.unobserve(C)):()=>!0}),[C]),(0,n.useEffect)((()=>{S({type:"reset"}),w&&(S({type:"loading"}),setTimeout((()=>{V()}),300))}),[w,g.searchVersions,V]),(0,n.useEffect)((()=>{H.lastPage&&0!==H.lastPage&&V(H.lastPage)}),[V,H.lastPage]),(0,E.jsxs)(P.Z,{children:[(0,E.jsxs)(f.Z,{children:[(0,E.jsx)("title",{children:(0,y.p)(q())}),(0,E.jsx)("meta",{property:"robots",content:"noindex, follow"})]}),(0,E.jsxs)("div",{className:"container margin-vert--lg",children:[(0,E.jsx)(x.Z,{as:"h1",children:q()}),(0,E.jsxs)("form",{className:"row",onSubmit:e=>e.preventDefault(),children:[(0,E.jsx)("div",{className:s("col",_.searchQueryColumn,{"col--9":g.versioningEnabled,"col--12":!g.versioningEnabled}),children:(0,E.jsx)("input",{type:"search",name:"q",className:_.searchQueryInput,placeholder:(0,R.I)({id:"theme.SearchPage.inputPlaceholder",message:"Type your search here",description:"The placeholder for search page input"}),"aria-label":(0,R.I)({id:"theme.SearchPage.inputLabel",message:"Search",description:"The ARIA label for search page input"}),onChange:e=>A(e.target.value),value:w,autoComplete:"off",autoFocus:!0})}),g.versioningEnabled&&(0,E.jsx)(O,{docsSearchVersionsHelpers:g})]}),(0,E.jsxs)("div",{className:"row",children:[(0,E.jsx)("div",{className:s("col","col--8",_.searchResultsColumn),children:!!H.totalResults&&u(H.totalResults)}),(0,E.jsx)("div",{className:s("col","col--4","text--right",_.searchLogoColumn),children:(0,E.jsx)(l.Z,{to:"https://www.algolia.com/","aria-label":(0,R.I)({id:"theme.SearchPage.algoliaLabel",message:"Search by Algolia",description:"The ARIA label for Algolia mention"}),children:(0,E.jsx)("svg",{viewBox:"0 0 168 24",className:_.algoliaLogo,children:(0,E.jsxs)("g",{fill:"none",children:[(0,E.jsx)("path",{className:_.algoliaLogoPathFill,d:"M120.925 18.804c-4.386.02-4.386-3.54-4.386-4.106l-.007-13.336 2.675-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-10.846-2.18c.821 0 1.43-.047 1.855-.129v-2.719a6.334 6.334 0 0 0-1.574-.199 5.7 5.7 0 0 0-.897.069 2.699 2.699 0 0 0-.814.24c-.24.116-.439.28-.582.491-.15.212-.219.335-.219.656 0 .628.219.991.616 1.23s.938.362 1.615.362zm-.233-9.7c.883 0 1.629.109 2.231.328.602.218 1.088.525 1.444.915.363.396.609.922.76 1.483.157.56.232 1.175.232 1.85v6.874a32.5 32.5 0 0 1-1.868.314c-.834.123-1.772.185-2.813.185-.69 0-1.327-.069-1.895-.198a4.001 4.001 0 0 1-1.471-.636 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.803 0-.656.13-1.073.384-1.525a3.24 3.24 0 0 1 1.047-1.106c.445-.287.95-.492 1.532-.615a8.8 8.8 0 0 1 1.82-.185 8.404 8.404 0 0 1 1.972.24v-.438c0-.307-.035-.6-.11-.874a1.88 1.88 0 0 0-.384-.73 1.784 1.784 0 0 0-.724-.493 3.164 3.164 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.735 7.735 0 0 0-1.26.307l-.321-2.192c.335-.117.834-.233 1.478-.349a10.98 10.98 0 0 1 2.073-.178zm52.842 9.626c.822 0 1.43-.048 1.854-.13V13.7a6.347 6.347 0 0 0-1.574-.199c-.294 0-.595.021-.896.069a2.7 2.7 0 0 0-.814.24 1.46 1.46 0 0 0-.582.491c-.15.212-.218.335-.218.656 0 .628.218.991.615 1.23.404.245.938.362 1.615.362zm-.226-9.694c.883 0 1.629.108 2.231.327.602.219 1.088.526 1.444.915.355.39.609.923.759 1.483a6.8 6.8 0 0 1 .233 1.852v6.873c-.41.088-1.034.19-1.868.314-.834.123-1.772.184-2.813.184-.69 0-1.327-.068-1.895-.198a4.001 4.001 0 0 1-1.471-.635 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.804 0-.656.13-1.073.384-1.524.26-.45.608-.82 1.047-1.107.445-.286.95-.491 1.532-.614a8.803 8.803 0 0 1 2.751-.13c.329.034.671.096 1.04.185v-.437a3.3 3.3 0 0 0-.109-.875 1.873 1.873 0 0 0-.384-.731 1.784 1.784 0 0 0-.724-.492 3.165 3.165 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.75 7.75 0 0 0-1.26.307l-.321-2.193c.335-.116.834-.232 1.478-.348a11.633 11.633 0 0 1 2.073-.177zm-8.034-1.271a1.626 1.626 0 0 1-1.628-1.62c0-.895.725-1.62 1.628-1.62.904 0 1.63.725 1.63 1.62 0 .895-.733 1.62-1.63 1.62zm1.348 13.22h-2.689V7.27l2.69-.423v11.956zm-4.714 0c-4.386.02-4.386-3.54-4.386-4.107l-.008-13.336 2.676-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-8.698-5.903c0-1.156-.253-2.119-.746-2.788-.493-.677-1.183-1.01-2.067-1.01-.882 0-1.574.333-2.065 1.01-.493.676-.733 1.632-.733 2.788 0 1.168.246 1.953.74 2.63.492.683 1.183 1.018 2.066 1.018.882 0 1.574-.342 2.067-1.019.492-.683.738-1.46.738-2.63zm2.737-.007c0 .902-.13 1.584-.397 2.33a5.52 5.52 0 0 1-1.128 1.906 4.986 4.986 0 0 1-1.752 1.223c-.685.286-1.739.45-2.265.45-.528-.006-1.574-.157-2.252-.45a5.096 5.096 0 0 1-1.744-1.223c-.487-.527-.863-1.162-1.137-1.906a6.345 6.345 0 0 1-.41-2.33c0-.902.123-1.77.397-2.508a5.554 5.554 0 0 1 1.15-1.892 5.133 5.133 0 0 1 1.75-1.216c.679-.287 1.425-.423 2.232-.423.808 0 1.553.142 2.237.423a4.88 4.88 0 0 1 1.753 1.216 5.644 5.644 0 0 1 1.135 1.892c.287.738.431 1.606.431 2.508zm-20.138 0c0 1.12.246 2.363.738 2.882.493.52 1.13.78 1.91.78.424 0 .828-.062 1.204-.178.377-.116.677-.253.917-.417V9.33a10.476 10.476 0 0 0-1.766-.226c-.971-.028-1.71.37-2.23 1.004-.513.636-.773 1.75-.773 2.788zm7.438 5.274c0 1.824-.466 3.156-1.404 4.004-.936.846-2.367 1.27-4.296 1.27-.705 0-2.17-.137-3.34-.396l.431-2.118c.98.205 2.272.26 2.95.26 1.074 0 1.84-.219 2.299-.656.459-.437.684-1.086.684-1.948v-.437a8.07 8.07 0 0 1-1.047.397c-.43.13-.93.198-1.492.198-.739 0-1.41-.116-2.018-.349a4.206 4.206 0 0 1-1.567-1.025c-.431-.45-.774-1.017-1.013-1.694-.24-.677-.363-1.885-.363-2.773 0-.834.13-1.88.384-2.577.26-.696.629-1.298 1.129-1.796.493-.498 1.095-.881 1.8-1.162a6.605 6.605 0 0 1 2.428-.457c.87 0 1.67.109 2.45.24.78.129 1.444.265 1.985.415V18.17zM6.972 6.677v1.627c-.712-.446-1.52-.67-2.425-.67-.585 0-1.045.13-1.38.391a1.24 1.24 0 0 0-.502 1.03c0 .425.164.765.494 1.02.33.256.835.532 1.516.83.447.192.795.356 1.045.495.25.138.537.332.862.582.324.25.563.548.718.894.154.345.23.741.23 1.188 0 .947-.334 1.691-1.004 2.234-.67.542-1.537.814-2.601.814-1.18 0-2.16-.229-2.936-.686v-1.708c.84.628 1.814.942 2.92.942.585 0 1.048-.136 1.388-.407.34-.271.51-.646.51-1.125 0-.287-.1-.55-.302-.79-.203-.24-.42-.42-.655-.542-.234-.123-.585-.29-1.053-.503a61.27 61.27 0 0 1-.582-.271 13.67 13.67 0 0 1-.55-.287 4.275 4.275 0 0 1-.567-.351 6.92 6.92 0 0 1-.455-.4c-.18-.17-.31-.34-.39-.51-.08-.17-.155-.37-.224-.598a2.553 2.553 0 0 1-.104-.742c0-.915.333-1.638.998-2.17.664-.532 1.523-.798 2.576-.798.968 0 1.793.17 2.473.51zm7.468 5.696v-.287c-.022-.607-.187-1.088-.495-1.444-.309-.357-.75-.535-1.324-.535-.532 0-.99.194-1.373.583-.382.388-.622.949-.717 1.683h3.909zm1.005 2.792v1.404c-.596.34-1.383.51-2.362.51-1.255 0-2.255-.377-3-1.132-.744-.755-1.116-1.744-1.116-2.968 0-1.297.34-2.316 1.021-3.055.68-.74 1.548-1.11 2.6-1.11 1.033 0 1.852.323 2.458.966.606.644.91 1.572.91 2.784 0 .33-.033.676-.096 1.038h-5.314c.107.702.405 1.239.894 1.611.49.372 1.106.558 1.85.558.862 0 1.58-.202 2.155-.606zm6.605-1.77h-1.212c-.596 0-1.045.116-1.349.35-.303.234-.454.532-.454.894 0 .372.117.664.35.877.235.213.575.32 1.022.32.51 0 .912-.142 1.204-.424.293-.281.44-.651.44-1.108v-.91zm-4.068-2.554V9.325c.627-.361 1.457-.542 2.489-.542 2.116 0 3.175 1.026 3.175 3.08V17h-1.548v-.957c-.415.68-1.143 1.02-2.186 1.02-.766 0-1.38-.22-1.843-.661-.462-.442-.694-1.003-.694-1.684 0-.776.293-1.38.878-1.81.585-.431 1.404-.647 2.457-.647h1.34V11.8c0-.554-.133-.971-.399-1.253-.266-.282-.707-.423-1.324-.423a4.07 4.07 0 0 0-2.345.718zm9.333-1.93v1.42c.394-1 1.101-1.5 2.123-1.5.148 0 .313.016.494.048v1.531a1.885 1.885 0 0 0-.75-.143c-.542 0-.989.24-1.34.718-.351.479-.527 1.048-.527 1.707V17h-1.563V8.91h1.563zm5.01 4.084c.022.82.272 1.492.75 2.019.479.526 1.15.79 2.01.79.639 0 1.235-.176 1.788-.527v1.404c-.521.319-1.186.479-1.995.479-1.265 0-2.276-.4-3.031-1.197-.755-.798-1.133-1.792-1.133-2.984 0-1.16.38-2.151 1.14-2.975.761-.825 1.79-1.237 3.088-1.237.702 0 1.346.149 1.93.447v1.436a3.242 3.242 0 0 0-1.77-.495c-.84 0-1.513.266-2.019.798-.505.532-.758 1.213-.758 2.042zM40.24 5.72v4.579c.458-1 1.293-1.5 2.505-1.5.787 0 1.42.245 1.899.734.479.49.718 1.17.718 2.042V17h-1.564v-5.106c0-.553-.14-.98-.422-1.284-.282-.303-.652-.455-1.11-.455-.531 0-1.002.202-1.411.606-.41.405-.615 1.022-.615 1.851V17h-1.563V5.72h1.563zm14.966 10.02c.596 0 1.096-.253 1.5-.758.404-.506.606-1.157.606-1.955 0-.915-.202-1.62-.606-2.114-.404-.495-.92-.742-1.548-.742-.553 0-1.05.224-1.491.67-.442.447-.662 1.133-.662 2.058 0 .958.212 1.67.638 2.138.425.469.946.703 1.563.703zM53.004 5.72v4.42c.574-.894 1.388-1.341 2.44-1.341 1.022 0 1.857.383 2.506 1.149.649.766.973 1.781.973 3.047 0 1.138-.309 2.109-.925 2.912-.617.803-1.463 1.205-2.537 1.205-1.075 0-1.894-.447-2.457-1.34V17h-1.58V5.72h1.58zm9.908 11.104l-3.223-7.913h1.739l1.005 2.632 1.26 3.415c.096-.32.48-1.458 1.15-3.415l.909-2.632h1.66l-2.92 7.866c-.777 2.074-1.963 3.11-3.559 3.11a2.92 2.92 0 0 1-.734-.079v-1.34c.17.042.351.064.543.064 1.032 0 1.755-.57 2.17-1.708z"}),(0,E.jsx)("path",{fill:"#5468FF",d:"M78.988.938h16.594a2.968 2.968 0 0 1 2.966 2.966V20.5a2.967 2.967 0 0 1-2.966 2.964H78.988a2.967 2.967 0 0 1-2.966-2.964V3.897A2.961 2.961 0 0 1 78.988.938z"}),(0,E.jsx)("path",{fill:"white",d:"M89.632 5.967v-.772a.978.978 0 0 0-.978-.977h-2.28a.978.978 0 0 0-.978.977v.793c0 .088.082.15.171.13a7.127 7.127 0 0 1 1.984-.28c.65 0 1.295.088 1.917.259.082.02.164-.04.164-.13m-6.248 1.01l-.39-.389a.977.977 0 0 0-1.382 0l-.465.465a.973.973 0 0 0 0 1.38l.383.383c.062.061.15.047.205-.014.226-.307.472-.601.746-.874.281-.28.568-.526.883-.751.068-.042.075-.137.02-.2m4.16 2.453v3.341c0 .096.104.165.192.117l2.97-1.537c.068-.034.089-.117.055-.184a3.695 3.695 0 0 0-3.08-1.866c-.068 0-.136.054-.136.13m0 8.048a4.489 4.489 0 0 1-4.49-4.482 4.488 4.488 0 0 1 4.49-4.482 4.488 4.488 0 0 1 4.489 4.482 4.484 4.484 0 0 1-4.49 4.482m0-10.85a6.363 6.363 0 1 0 0 12.729 6.37 6.37 0 0 0 6.372-6.368 6.358 6.358 0 0 0-6.371-6.36"})]})})})})]}),H.items.length>0?(0,E.jsx)("main",{children:H.items.map(((e,t)=>{let{title:r,url:n,summary:i,breadcrumbs:a}=e;return(0,E.jsxs)("article",{className:_.searchResultItem,children:[(0,E.jsx)(x.Z,{as:"h2",className:_.searchResultItemHeading,children:(0,E.jsx)(l.Z,{to:n,dangerouslySetInnerHTML:{__html:r}})}),a.length>0&&(0,E.jsx)("nav",{"aria-label":"breadcrumbs",children:(0,E.jsx)("ul",{className:s("breadcrumbs",_.searchResultItemPath),children:a.map(((e,t)=>(0,E.jsx)("li",{className:"breadcrumbs__item",dangerouslySetInnerHTML:{__html:e}},t)))})}),i&&(0,E.jsx)("p",{className:_.searchResultItemSummary,dangerouslySetInnerHTML:{__html:i}})]},t)}))}):[w&&!H.loading&&(0,E.jsx)("p",{children:(0,E.jsx)(R.Z,{id:"theme.SearchPage.noResultsText",description:"The paragraph for empty search result",children:"No results were found"})},"no-results"),!!H.loading&&(0,E.jsx)("div",{className:_.loadingSpinner},"spinner")],H.hasMore&&(0,E.jsx)("div",{className:_.loader,ref:I,children:(0,E.jsx)(R.Z,{id:"theme.SearchPage.fetchingNewResults",description:"The paragraph for fetching new search results",children:"Fetching new results..."})})]})]})}function A(){return(0,E.jsx)(g.FG,{className:"search-page-wrapper",children:(0,E.jsx)(w,{})})}}}]); \ No newline at end of file diff --git a/assets/js/1a4e3797.7620272a.js.LICENSE.txt b/assets/js/1a4e3797.9c47db27.js.LICENSE.txt similarity index 100% rename from assets/js/1a4e3797.7620272a.js.LICENSE.txt rename to assets/js/1a4e3797.9c47db27.js.LICENSE.txt diff --git a/assets/js/1cd70467.94579b78.js b/assets/js/1cd70467.54c40b64.js similarity index 98% rename from assets/js/1cd70467.94579b78.js rename to assets/js/1cd70467.54c40b64.js index 06a477f3f..e399cd3ab 100644 --- a/assets/js/1cd70467.94579b78.js +++ b/assets/js/1cd70467.54c40b64.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6533],{82191:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var i=t(85893),s=t(11151);const o={title:"Million connections with Centrifugo",tags:["centrifuge","go"],description:"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",author:"Centrifugal team",authorTitle:"Let the Centrifugal force be with you",authorImageURL:"/img/logo_animated.svg",image:"/img/million_conns.jpg",hide_table_of_contents:!1},r=void 0,a={permalink:"/blog/2020/02/10/million-connections-with-centrifugo",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2020-02-10-million-connections-with-centrifugo.md",source:"@site/blog/2020-02-10-million-connections-with-centrifugo.md",title:"Million connections with Centrifugo",description:"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",date:"2020-02-10T00:00:00.000Z",tags:[{label:"centrifuge",permalink:"/blog/tags/centrifuge"},{label:"go",permalink:"/blog/tags/go"}],readingTime:3.045,hasTruncateMarker:!0,authors:[{name:"Centrifugal team",title:"Let the Centrifugal force be with you",imageURL:"/img/logo_animated.svg"}],frontMatter:{title:"Million connections with Centrifugo",tags:["centrifuge","go"],description:"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",author:"Centrifugal team",authorTitle:"Let the Centrifugal force be with you",authorImageURL:"/img/logo_animated.svg",image:"/img/million_conns.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Experimenting with QUIC and WebTransport",permalink:"/blog/2020/10/16/experimenting-with-quic-transport"}},c={authorsImageUrls:[void 0]},l=[];function d(e){const n={code:"code",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:"/img/million_conns.jpg"}),"\n",(0,i.jsx)(n.p,{children:"In order to get an understanding about possible hardware requirements for reasonably massive Centrifugo setup we made a test stand inside Kubernetes."}),"\n",(0,i.jsx)(n.p,{children:"Our goal was to run server based on Centrifuge library (the core of Centrifugo server) with one million WebSocket connections and send many messages to connected clients. While sending many messages we have been looking at delivery time latency. In fact we will see that about 30 million messages per minute (500k messages per second) will be delivered to connected clients and latency won't be larger than 200ms in 99 percentile."}),"\n",(0,i.jsx)(n.p,{children:"Server nodes have been run on machines with the following configuration:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"CPU Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz"}),"\n",(0,i.jsx)(n.li,{children:"Linux Debian 4.9.65-3+deb9u1 (2017-12-23) x86_64 GNU/Linux"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Some ",(0,i.jsx)(n.code,{children:"sysctl"})," values:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"fs.file-max = 3276750\nfs.nr_open = 1048576\nnet.ipv4.tcp_mem = 3086496\t4115330\t6172992\nnet.ipv4.tcp_rmem = 8192\t8388608\t16777216\nnet.ipv4.tcp_wmem = 4096\t4194394\t16777216\nnet.core.rmem_max = 33554432\nnet.core.wmem_max = 33554432\n"})}),"\n",(0,i.jsx)(n.p,{children:"Kubernetes used these machines as its nodes."}),"\n",(0,i.jsx)(n.p,{children:"We started 20 Centrifuge-based server pods. Our clients connected to server pods using Centrifuge Protobuf protocol. To scale horizontally we used Redis Engine and sharded it to 5 different Redis instances (each Redis instance consumes 1 CPU max)."}),"\n",(0,i.jsx)(n.p,{children:"To achieve many client connections we used 100 Kubernetes pods each generating about 10k client connections to server."}),"\n",(0,i.jsx)(n.p,{children:"Here are some numbers we achieved:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"1 million WebSocket connections"}),"\n",(0,i.jsx)(n.li,{children:"Each connection subscribed to 2 channels: one personal channel and one group channel (with 10 subscribers in it), i.e. we had about 1.1 million active channels at each moment."}),"\n",(0,i.jsxs)(n.li,{children:["28 million messages per minute (about 500k per second) ",(0,i.jsx)(n.strong,{children:"delivered"})," to clients"]}),"\n",(0,i.jsx)(n.li,{children:"200k per minute constant connect/disconnect rate to simulate real-life situation where clients connect/disconnect from server"}),"\n",(0,i.jsx)(n.li,{children:"200ms delivery latency in 99 percentile"}),"\n",(0,i.jsx)(n.li,{children:"The size of each published message was about 100 bytes"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And here are some numbers about final resource usage on server side (we don't actually interested in client side resource usage here):"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"40 CPU total for server nodes when load achieved values claimed above (20 pods, ~2 CPU each)"}),"\n",(0,i.jsx)(n.li,{children:"27 GB of RAM used mostly to handle 1 mln WebSocket connections, i.e. about 30kb RAM per connection"}),"\n",(0,i.jsx)(n.li,{children:"0.32 CPU usage on every Redis instance"}),"\n",(0,i.jsx)(n.li,{children:"100 mbit/sec rx \u0438 150 mbit/sec tx of network used on each server pod"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"The picture that demonstrates experiment (better to open image in new tab):"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Benchmark",src:t(86842).Z+"",width:"1860",height:"916"})}),"\n",(0,i.jsx)(n.p,{children:"This also demonstrates that to handle one million of WebSocket connections without many messages sent to clients you need about 10 CPU total for server nodes and about 5% of CPU on each of Redis instances. In this case CPU mostly spent on connect/disconnect flow, ping/pong frames, subscriptions to channels."}),"\n",(0,i.jsx)(n.p,{children:"If we enable history and history message recovery features we see an increased Redis CPU usage: 64% instead of 32% on the same workload. Other resources usage is pretty the same."}),"\n",(0,i.jsx)(n.p,{children:"The results mean that one can theoretically achieve the comparable numbers on single modern server machine. But numbers can vary a lot in case of different load scenarios. In this benchmark we looked at basic use case where we only connect many clients and send Publications to them. There are many features in Centrifuge library and in Centrifugo not covered by this artificial experiment. Also note that though benchmark was made for Centrifuge library for Centrifugo you can expect similar results."}),"\n",(0,i.jsx)(n.p,{children:"Read and write buffer sizes of websocket connections were set to 512 kb on server side (sizes of buffers affect memory usage), with Centrifugo this means that to reproduce the same configuration you need to set:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "websocket_read_buffer_size": 512,\n "websocket_write_buffer_size": 512\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},86842:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/benchmark-b670972866abdc8936d2aef84333dd0c.gif"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6533],{82191:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var i=t(85893),s=t(11151);const o={title:"Million connections with Centrifugo",tags:["centrifuge","go"],description:"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",author:"Centrifugal team",authorTitle:"Let the Centrifugal force be with you",authorImageURL:"/img/logo_animated.svg",image:"/img/million_conns.jpg",hide_table_of_contents:!1},r=void 0,a={permalink:"/blog/2020/02/10/million-connections-with-centrifugo",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2020-02-10-million-connections-with-centrifugo.md",source:"@site/blog/2020-02-10-million-connections-with-centrifugo.md",title:"Million connections with Centrifugo",description:"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",date:"2020-02-10T00:00:00.000Z",tags:[{label:"centrifuge",permalink:"/blog/tags/centrifuge"},{label:"go",permalink:"/blog/tags/go"}],readingTime:3.045,hasTruncateMarker:!0,authors:[{name:"Centrifugal team",title:"Let the Centrifugal force be with you",imageURL:"/img/logo_animated.svg"}],frontMatter:{title:"Million connections with Centrifugo",tags:["centrifuge","go"],description:"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",author:"Centrifugal team",authorTitle:"Let the Centrifugal force be with you",authorImageURL:"/img/logo_animated.svg",image:"/img/million_conns.jpg",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Experimenting with QUIC and WebTransport",permalink:"/blog/2020/10/16/experimenting-with-quic-transport"}},c={authorsImageUrls:[void 0]},l=[];function d(e){const n={code:"code",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:"/img/million_conns.jpg"}),"\n",(0,i.jsx)(n.p,{children:"In order to get an understanding about possible hardware requirements for reasonably massive Centrifugo setup we made a test stand inside Kubernetes."}),"\n",(0,i.jsx)(n.p,{children:"Our goal was to run server based on Centrifuge library (the core of Centrifugo server) with one million WebSocket connections and send many messages to connected clients. While sending many messages we have been looking at delivery time latency. In fact we will see that about 30 million messages per minute (500k messages per second) will be delivered to connected clients and latency won't be larger than 200ms in 99 percentile."}),"\n",(0,i.jsx)(n.p,{children:"Server nodes have been run on machines with the following configuration:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"CPU Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz"}),"\n",(0,i.jsx)(n.li,{children:"Linux Debian 4.9.65-3+deb9u1 (2017-12-23) x86_64 GNU/Linux"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Some ",(0,i.jsx)(n.code,{children:"sysctl"})," values:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"fs.file-max = 3276750\nfs.nr_open = 1048576\nnet.ipv4.tcp_mem = 3086496\t4115330\t6172992\nnet.ipv4.tcp_rmem = 8192\t8388608\t16777216\nnet.ipv4.tcp_wmem = 4096\t4194394\t16777216\nnet.core.rmem_max = 33554432\nnet.core.wmem_max = 33554432\n"})}),"\n",(0,i.jsx)(n.p,{children:"Kubernetes used these machines as its nodes."}),"\n",(0,i.jsx)(n.p,{children:"We started 20 Centrifuge-based server pods. Our clients connected to server pods using Centrifuge Protobuf protocol. To scale horizontally we used Redis Engine and sharded it to 5 different Redis instances (each Redis instance consumes 1 CPU max)."}),"\n",(0,i.jsx)(n.p,{children:"To achieve many client connections we used 100 Kubernetes pods each generating about 10k client connections to server."}),"\n",(0,i.jsx)(n.p,{children:"Here are some numbers we achieved:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"1 million WebSocket connections"}),"\n",(0,i.jsx)(n.li,{children:"Each connection subscribed to 2 channels: one personal channel and one group channel (with 10 subscribers in it), i.e. we had about 1.1 million active channels at each moment."}),"\n",(0,i.jsxs)(n.li,{children:["28 million messages per minute (about 500k per second) ",(0,i.jsx)(n.strong,{children:"delivered"})," to clients"]}),"\n",(0,i.jsx)(n.li,{children:"200k per minute constant connect/disconnect rate to simulate real-life situation where clients connect/disconnect from server"}),"\n",(0,i.jsx)(n.li,{children:"200ms delivery latency in 99 percentile"}),"\n",(0,i.jsx)(n.li,{children:"The size of each published message was about 100 bytes"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"And here are some numbers about final resource usage on server side (we don't actually interested in client side resource usage here):"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"40 CPU total for server nodes when load achieved values claimed above (20 pods, ~2 CPU each)"}),"\n",(0,i.jsx)(n.li,{children:"27 GB of RAM used mostly to handle 1 mln WebSocket connections, i.e. about 30kb RAM per connection"}),"\n",(0,i.jsx)(n.li,{children:"0.32 CPU usage on every Redis instance"}),"\n",(0,i.jsx)(n.li,{children:"100 mbit/sec rx \u0438 150 mbit/sec tx of network used on each server pod"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"The picture that demonstrates experiment (better to open image in new tab):"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Benchmark",src:t(64551).Z+"",width:"1860",height:"916"})}),"\n",(0,i.jsx)(n.p,{children:"This also demonstrates that to handle one million of WebSocket connections without many messages sent to clients you need about 10 CPU total for server nodes and about 5% of CPU on each of Redis instances. In this case CPU mostly spent on connect/disconnect flow, ping/pong frames, subscriptions to channels."}),"\n",(0,i.jsx)(n.p,{children:"If we enable history and history message recovery features we see an increased Redis CPU usage: 64% instead of 32% on the same workload. Other resources usage is pretty the same."}),"\n",(0,i.jsx)(n.p,{children:"The results mean that one can theoretically achieve the comparable numbers on single modern server machine. But numbers can vary a lot in case of different load scenarios. In this benchmark we looked at basic use case where we only connect many clients and send Publications to them. There are many features in Centrifuge library and in Centrifugo not covered by this artificial experiment. Also note that though benchmark was made for Centrifuge library for Centrifugo you can expect similar results."}),"\n",(0,i.jsx)(n.p,{children:"Read and write buffer sizes of websocket connections were set to 512 kb on server side (sizes of buffers affect memory usage), with Centrifugo this means that to reproduce the same configuration you need to set:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "websocket_read_buffer_size": 512,\n "websocket_write_buffer_size": 512\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},64551:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/benchmark-b670972866abdc8936d2aef84333dd0c.gif"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1d3c9151.84a6d2bd.js b/assets/js/1d3c9151.14f00664.js similarity index 96% rename from assets/js/1d3c9151.84a6d2bd.js rename to assets/js/1d3c9151.14f00664.js index db54734c1..33834ce6c 100644 --- a/assets/js/1d3c9151.84a6d2bd.js +++ b/assets/js/1d3c9151.14f00664.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3981],{20999:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var i=t(85893),s=t(11151);const o={id:"singleflight",title:"Singleflight"},r=void 0,l={id:"pro/singleflight",title:"Singleflight",description:"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.",source:"@site/versioned_docs/version-4/pro/singleflight.md",sourceDirName:"pro",slug:"/pro/singleflight",permalink:"/docs/4/pro/singleflight",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/singleflight.md",tags:[],version:"4",frontMatter:{id:"singleflight",title:"Singleflight"},sidebar:"Pro",previous:{title:"Faster performance",permalink:"/docs/4/pro/performance"},next:{title:"Message batching control",permalink:"/docs/4/pro/client_message_batching"}},a={},c=[];function d(e){const n={admonition:"admonition",code:"code",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["Centrifugo PRO provides an additional boolean option ",(0,i.jsx)(n.code,{children:"use_singleflight"})," (default ",(0,i.jsx)(n.code,{children:"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 ",(0,i.jsx)(n.code,{children:"singleflight"}),"."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Singleflight",src:t(11117).Z+"",width:"4259",height:"858"})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"While it can seem similar, singleflight is not a cache. It only combines identical parallel requests into one. If requests come one after another \u2013 they will be sent separately to the broker or presence storage."})}),"\n",(0,i.jsx)(n.p,{children:"This option can radically reduce a load on a broker in the following situations:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and in case of massive reconnect scenario try to access history simultaneously to restore a state (whether manually using history API or over automatic recovery feature)"}),"\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and positioning feature is on so Centrifugo tracks client position"}),"\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and in case of massive reconnect scenario try to call presence or presence stats simultaneously"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Using this option only makes sense with remote engine (Redis, KeyDB, Tarantool), it won't provide a benefit in case of using a Memory engine."}),"\n",(0,i.jsx)(n.p,{children:"To enable:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "use_singleflight": true\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Or via ",(0,i.jsx)(n.code,{children:"CENTRIFUGO_USE_SINGLEFLIGHT"})," environment variable."]})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},11117:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/singleflight-35650f07c8cda65fec55fd490157a6a0.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3981],{20999:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var i=t(85893),s=t(11151);const o={id:"singleflight",title:"Singleflight"},r=void 0,l={id:"pro/singleflight",title:"Singleflight",description:"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.",source:"@site/versioned_docs/version-4/pro/singleflight.md",sourceDirName:"pro",slug:"/pro/singleflight",permalink:"/docs/4/pro/singleflight",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/singleflight.md",tags:[],version:"4",frontMatter:{id:"singleflight",title:"Singleflight"},sidebar:"Pro",previous:{title:"Faster performance",permalink:"/docs/4/pro/performance"},next:{title:"Message batching control",permalink:"/docs/4/pro/client_message_batching"}},a={},c=[];function d(e){const n={admonition:"admonition",code:"code",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["Centrifugo PRO provides an additional boolean option ",(0,i.jsx)(n.code,{children:"use_singleflight"})," (default ",(0,i.jsx)(n.code,{children:"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 ",(0,i.jsx)(n.code,{children:"singleflight"}),"."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Singleflight",src:t(22568).Z+"",width:"4259",height:"858"})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"While it can seem similar, singleflight is not a cache. It only combines identical parallel requests into one. If requests come one after another \u2013 they will be sent separately to the broker or presence storage."})}),"\n",(0,i.jsx)(n.p,{children:"This option can radically reduce a load on a broker in the following situations:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and in case of massive reconnect scenario try to access history simultaneously to restore a state (whether manually using history API or over automatic recovery feature)"}),"\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and positioning feature is on so Centrifugo tracks client position"}),"\n",(0,i.jsx)(n.li,{children:"Many clients subscribed to the same channel and in case of massive reconnect scenario try to call presence or presence stats simultaneously"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Using this option only makes sense with remote engine (Redis, KeyDB, Tarantool), it won't provide a benefit in case of using a Memory engine."}),"\n",(0,i.jsx)(n.p,{children:"To enable:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "use_singleflight": true\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Or via ",(0,i.jsx)(n.code,{children:"CENTRIFUGO_USE_SINGLEFLIGHT"})," environment variable."]})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},22568:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/singleflight-35650f07c8cda65fec55fd490157a6a0.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1d4d4d48.240887d6.js b/assets/js/1d4d4d48.7e48f500.js similarity index 99% rename from assets/js/1d4d4d48.240887d6.js rename to assets/js/1d4d4d48.7e48f500.js index 4e842f0ab..f403501fb 100644 --- a/assets/js/1d4d4d48.240887d6.js +++ b/assets/js/1d4d4d48.7e48f500.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9727],{21363:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>d,toc:()=>l});var r=s(85893),i=s(11151);const t={id:"proxy",title:"Proxy to backend"},o=void 0,d={id:"server/proxy",title:"Proxy to backend",description:"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.",source:"@site/versioned_docs/version-3/server/proxy.md",sourceDirName:"server",slug:"/server/proxy",permalink:"/docs/3/server/proxy",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/server/proxy.md",tags:[],version:"3",frontMatter:{id:"proxy",title:"Proxy to backend"},sidebar:"Guides",previous:{title:"Engines, scalability",permalink:"/docs/3/server/engines"},next:{title:"History and recovery",permalink:"/docs/3/server/history_and_recovery"}},c={},l=[{value:"HTTP proxy",id:"http-proxy",level:2},{value:"HTTP request structure",id:"http-request-structure",level:3},{value:"Proxy HTTP headers",id:"proxy-http-headers",level:3},{value:"Proxy GRPC metadata",id:"proxy-grpc-metadata",level:3},{value:"Connect proxy",id:"connect-proxy",level:3},{value:"Connect request fields",id:"connect-request-fields",level:4},{value:"Connect result fields",id:"connect-result-fields",level:4},{value:"Options",id:"options",level:4},{value:"Example",id:"example",level:4},{value:"Refresh proxy",id:"refresh-proxy",level:3},{value:"Refresh request fields",id:"refresh-request-fields",level:4},{value:"Refresh result fields",id:"refresh-result-fields",level:4},{value:"Options",id:"options-1",level:4},{value:"RPC proxy",id:"rpc-proxy",level:3},{value:"RPC request fields",id:"rpc-request-fields",level:4},{value:"RPC result fields",id:"rpc-result-fields",level:4},{value:"Options",id:"options-2",level:4},{value:"Subscribe proxy",id:"subscribe-proxy",level:3},{value:"Subscribe request fields",id:"subscribe-request-fields",level:4},{value:"Subscribe result fields",id:"subscribe-result-fields",level:4},{value:"Override object",id:"override-object",level:4},{value:"Options",id:"options-3",level:4},{value:"Publish proxy",id:"publish-proxy",level:3},{value:"Publish request fields",id:"publish-request-fields",level:4},{value:"Publish result fields",id:"publish-result-fields",level:4},{value:"Options",id:"options-4",level:4},{value:"Return custom error",id:"return-custom-error",level:3},{value:"Return custom disconnect",id:"return-custom-disconnect",level:3},{value:"GRPC proxy",id:"grpc-proxy",level:2},{value:"GRPC proxy options",id:"grpc-proxy-options",level:3},{value:"proxy_grpc_cert_file",id:"proxy_grpc_cert_file",level:4},{value:"proxy_grpc_credentials_key",id:"proxy_grpc_credentials_key",level:4},{value:"proxy_grpc_credentials_value",id:"proxy_grpc_credentials_value",level:4},{value:"GRPC proxy example",id:"grpc-proxy-example",level:3},{value:"Header proxy rules",id:"header-proxy-rules",level:2},{value:"Binary mode",id:"binary-mode",level:2},{value:"Granular proxy mode",id:"granular-proxy-mode",level:2},{value:"Enable granular proxy mode",id:"enable-granular-proxy-mode",level:3},{value:"Defining a list of proxies",id:"defining-a-list-of-proxies",level:3},{value:"Granular connect and refresh",id:"granular-connect-and-refresh",level:3},{value:"Granular subscribe and publish",id:"granular-subscribe-and-publish",level:3},{value:"Granular RPC",id:"granular-rpc",level:3}];function a(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.p,{children:"The list of events that can be proxied:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Connect \u2013 called when a client connects to Centrifugo, so it's possible to authenticate user, return custom data to a client, subscribe connection to several channels, attach meta information to the connection, and so on. Works for bidirectional and unidirectional transports."}),"\n",(0,r.jsx)(n.li,{children:"Refresh - called when a client session is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical connection liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."}),"\n",(0,r.jsx)(n.li,{children:"Subscribe - called when clients try to subscribe on a channel, so it's possible to check permissions and return custom initial subscription data. Works for bidirectional transports only."}),"\n",(0,r.jsx)(n.li,{children:"Publish - called when a client tries to publish into a channel, so it's possible to check permissions and optionally modify publication data. Works for bidirectional transports only."}),"\n",(0,r.jsx)(n.li,{children:"RPC - called when a client sends RPC, you can do whatever logic you need based on a client-provided RPC method and params. Works for bidirectional transports only."}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"At the moment Centrifugo can proxy these events over two protocols:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"HTTP (JSON payloads)"}),"\n",(0,r.jsx)(n.li,{children:"GRPC (Protobuf messages)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"http-proxy",children:"HTTP proxy"}),"\n",(0,r.jsx)(n.p,{children:"HTTP proxy in Centrifugo converts client connection events into HTTP call to the application backend."}),"\n",(0,r.jsx)(n.h3,{id:"http-request-structure",children:"HTTP request structure"}),"\n",(0,r.jsxs)(n.p,{children:["All proxy calls are ",(0,r.jsx)(n.strong,{children:"HTTP POST"})," requests that will be sent from Centrifugo to configured endpoints with a configured timeout. These requests will have some headers copied from the original client request (see details below) and include JSON body which varies depending on call type (for example data sent by a client in RPC call etc, see more details about JSON bodies below)."]}),"\n",(0,r.jsx)(n.h3,{id:"proxy-http-headers",children:"Proxy HTTP headers"}),"\n",(0,r.jsx)(n.p,{children:"The good thing about Centrifugo HTTP proxy is that it transparently proxies original HTTP request headers in a request to app backend. In most cases this allows achieving transparent authentication on the application backend side. But it's required to provide an explicit list of HTTP headers you want to be proxied, for example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Alternatively, you can set a list of headers via an environment variable (space separated):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'export CENTRIFUGO_PROXY_HTTP_HEADERS="Cookie User-Agent X-B3-TraceId X-B3-SpanId" ./centrifugo\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Centrifugo forces the",(0,r.jsx)(n.code,{children:" Content-Type"})," header to be ",(0,r.jsx)(n.code,{children:"application/json"})," in all HTTP proxy requests since it sends the body in JSON format to the application backend."]})}),"\n",(0,r.jsx)(n.h3,{id:"proxy-grpc-metadata",children:"Proxy GRPC metadata"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.a,{href:"/docs/3/transports/uni_grpc",children:"GRPC unidirectional stream"})," is used as a client transport then you may want to proxy GRPC metadata from the client request. In this case you may configure ",(0,r.jsx)(n.code,{children:"proxy_grpc_metadata"})," option. This is an array of string metadata keys which will be proxied. These metadata keys transformed to HTTP headers of proxy request. By default no metadata keys are proxied."]}),"\n",(0,r.jsxs)(n.p,{children:["See below ",(0,r.jsx)(n.a,{href:"#header-proxy-rules",children:"the table of rules"})," how metadata and headers proxied in transport/proxy different scenarios."]}),"\n",(0,r.jsx)(n.h3,{id:"connect-proxy",children:"Connect proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_connect_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 connection requests ",(0,r.jsx)(n.strong,{children:"without JWT set"})," will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," URL endpoint. On your backend side, you can authenticate the incoming connection and return client credentials to Centrifugo in response to the proxied request."]}),"\n",(0,r.jsx)(n.admonition,{type:"danger",children:(0,r.jsxs)(n.p,{children:["Make sure you properly configured ",(0,r.jsx)(n.a,{href:"/docs/3/server/configuration#allowed_origins",children:"allowed_origins"})," Centrifugo option or check request origin on your backend side upon receiving connect request from Centrifugo. Otherwise, your site can be vulnerable to CSRF attacks if you are using WebSocket transport for client connections."]})}),"\n",(0,r.jsxs)(n.p,{children:["Yes, this means you don't need to generate JWT and pass it to a client-side and can rely on a cookie while authenticating the user. ",(0,r.jsx)(n.strong,{children:"Centrifugo should work on the same domain in this case so your site cookie could be passed to Centrifugo by browsers"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you want to pass some custom authentication token from a client side (not in Centrifugo JWT format) but force request to be proxied then you may put it in a cookie or use connection request custom ",(0,r.jsx)(n.code,{children:"data"})," field (available in all our transports). This ",(0,r.jsx)(n.code,{children:"data"})," can contain arbitrary payload you want to pass from a client to a server."]})}),"\n",(0,r.jsxs)(n.p,{children:["This also means that ",(0,r.jsx)(n.strong,{children:"every"})," new connection from a user will result in an HTTP POST request to your application backend. While with JWT token you usually generate it once on application page reload, if client reconnects due to Centrifugo restart or internet connection loss it uses the same JWT it had before thus usually no additional requests are generated during reconnect process (until JWT expired)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(40330).Z+"",width:"2600",height:"1032"})}),"\n",(0,r.jsxs)(n.p,{children:["Payload example that will be sent to app backend when client without token wants to establish a connection with Centrifugo and ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," is set to non-empty URL string:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"user": "56"}}\n'})}),"\n",(0,r.jsx)(n.p,{children:"This response allows connecting and tells Centrifugo the ID of a user. See below the full list of supported fields in the result."}),"\n",(0,r.jsx)(n.p,{children:"Several app examples which use connect proxy can be found in our blog:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"With NodeJS"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/11/04/integrating-with-django-building-chat-application",children:"With Django"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",children:"With Laravel"})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"connect-request-fields",children:"Connect request fields"}),"\n",(0,r.jsx)(n.p,{children:"This is what sent from Centrifugo to application backend in case of connect proxy request."}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional name of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"version"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional version of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from the client in base64 format (if the binary proxy mode is used)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"Array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"list of server-side channels client want to subscribe to, the application server must check permissions and add allowed channels to result"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"connect-result-fields",children:"Connect result fields"}),"\n",(0,r.jsxs)(n.p,{children:["This is what application returns to Centrifugo inside ",(0,r.jsx)(n.code,{children:"result"})," field in case of connect proxy request."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"user ID (calculated on app backend based on request cookie header for example). Return it as an empty string for accepting unauthenticated requests"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a timestamp when connection must be considered expired. If not set or set to ",(0,r.jsx)(n.code,{children:"0"})," connection won't expire at all"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in connect command response."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in the connect command response for binary connections, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["allows providing a list of server-side channels to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"subs"}),(0,r.jsx)(n.td,{children:"map of SubscribeOptions"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["map of channels with options to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsxs)(n.td,{children:["JSON object (ex. ",(0,r.jsx)(n.code,{children:'{"key": "value"}'}),")"]}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a custom data to attach to connection (this ",(0,r.jsx)(n.strong,{children:"won't be exposed to client-side"}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_connect_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h4,{id:"example",children:"Example"}),"\n",(0,r.jsxs)(n.p,{children:["Here is the simplest example of the connect handler in Tornado Python framework (note that in a real system you need to authenticate the user on your backend side, here we just return ",(0,r.jsx)(n.code,{children:'"56"'})," as user ID):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"class CentrifugoConnectHandler(tornado.web.RequestHandler):\n\n def check_xsrf_cookie(self):\n pass\n\n def post(self):\n self.set_header('Content-Type', 'application/json; charset=\"utf-8\"')\n data = json.dumps({\n 'result': {\n 'user': '56'\n }\n })\n self.write(data)\n\n\ndef main():\n options.parse_command_line()\n app = tornado.web.Application([\n (r'/centrifugo/connect', CentrifugoConnectHandler),\n ])\n app.listen(3000)\n tornado.ioloop.IOLoop.instance().start()\n\n\nif __name__ == '__main__':\n main()\n"})}),"\n",(0,r.jsx)(n.p,{children:"This example should help you to implement a similar HTTP handler in any language/framework you are using on the backend side."}),"\n",(0,r.jsxs)(n.p,{children:["We also have a tutorial in the blog about ",(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"Centrifugo integration with NodeJS"})," which uses connect proxy and native session middleware of Express.js to authenticate connections. Even if you are not using NodeJS on a backend a tutorial can help you understand the idea."]}),"\n",(0,r.jsx)(n.h3,{id:"refresh-proxy",children:"Refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_refresh_endpoint": "http://localhost:3000/centrifugo/refresh",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_refresh_endpoint"})," when it's time to refresh the connection. Centrifugo itself will ask your backend about connection validity instead of refresh workflow on the client-side."]}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in refresh request (when the connection is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"refresh-request-fields",children:"Refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"refresh-result-fields",children:"Refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-1",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_refresh_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"rpc-proxy",children:"RPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_rpc_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["RPC calls over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_rpc_endpoint"}),". This allows a developer to utilize WebSocket (or SockJS) connection in a bidirectional way."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in RPC request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "method": "getCurrentPrice",\n "data":{"params": {"object_id": 12}}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"data": {"answer": "2019"}}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"rpc-request-fields",children:"RPC request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"method"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"an RPC method string, if the client does not use named RPC call then method will be omitted"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC custom data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"rpc-result-fields",children:"RPC result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC response - any valid JSON is supported"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["can be set instead of ",(0,r.jsx)(n.code,{children:"data"})," for binary response encoded in base64 format"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-2",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_rpc_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return a custom error."}),"\n",(0,r.jsx)(n.h3,{id:"subscribe-proxy",children:"Subscribe proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 subscribe requests sent over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_subscribe_endpoint"}),". This allows you to check the access of the client to a channel."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Subscribe proxy does not proxy ",(0,r.jsx)(n.a,{href:"/docs/3/server/channels#private-channel-prefix",children:"private"})," and ",(0,r.jsx)(n.a,{href:"/docs/3/server/channels#user-channel-boundary",children:"user-limited"})," channels at the moment"]}),". That's because those are already providing a level of security (user-limited channels check current user ID, private channels require subscription token). In some cases you may use subscribe proxy as a replacement for private channels actually: if you prefer to check permissions using the proxy to backend mechanism \u2013 just stop using ",(0,r.jsx)(n.code,{children:"$"})," prefixes in channels, properly configure subscribe proxy and validate subscriptions upon proxy from Centrifugo to your backend (issued each time user tries to subscribe on a channel for which subscribe proxy enabled)."]})}),"\n",(0,r.jsxs)(n.p,{children:["Unlike proxy types described above subscribe proxy must be enabled per channel namespace. This means that every namespace (including global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," that enables subscribe proxy for channels in a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable subscribe proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "proxy_subscribe": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in subscribe request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if subscription is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-request-fields",children:"Subscribe request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to subscribe to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"custom data from client sent with subscription request (this field will only be set if provided by a client on subscribe). Available since Centrifugo v3.1.1"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional subscription data from the client in base64 format (if the binary proxy mode is used). Available since Centrifugo v3.1.1"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-result-fields",children:"Subscribe result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary connection channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"override"}),(0,r.jsx)(n.td,{children:"Override object"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"presence"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override presence"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"position"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override position"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"recover"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow subscribing."}),"\n",(0,r.jsx)(n.h4,{id:"options-3",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_subscribe_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"publish-proxy",children:"Publish proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 publish calls sent by a client will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_publish_endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"This request happens BEFORE a message is published to a channel, so your backend can validate whether a client can publish data to a channel. An important thing here is that publication to the channel can fail after your backend successfully validated publish request (for example publish to Redis by Centrifugo returned an error). In this case, your backend won't know about the error that happened but this error will propagate to the client-side."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(28719).Z+"",width:"2600",height:"1098"})}),"\n",(0,r.jsxs)(n.p,{children:["Like the subscribe proxy, publish proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_publish"})," that enables publish proxy for channels in the namespace. All other namespace options will be taken into account before making a proxy request, so you also need to turn on the ",(0,r.jsx)(n.code,{children:"publish"})," option too."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable publish proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_publish"})," and ",(0,r.jsx)(n.code,{children:"publish"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "publish": true,\n "proxy_publish": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "publish": true,\n "proxy_publish": true\n }]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Keep in mind that this will only work if the ",(0,r.jsx)(n.code,{children:"publish"})," channel option is on for a channel namespace (or for a global top-level namespace)."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in a publish request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index",\n "data":{"input":"hello"}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if publish is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"publish-request-fields",children:"Publish request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to publish to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"publish-result-fields",children:"Publish result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["an optional JSON data to send into a channel ",(0,r.jsx)(n.strong,{children:"instead of"})," original data sent by a client"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary data encoded in base64 format, the meaning is the same as for data above, will be decoded to raw bytes on Centrifugo side before publishing"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"skip_history"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["when set to ",(0,r.jsx)(n.code,{children:"true"})," Centrifugo won't save publication to the channel history"]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow publishing."}),"\n",(0,r.jsx)(n.h4,{id:"options-4",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_publish_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-error",children:"Return custom error"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains an error to return it to the client:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 1000,\n "message": "custom error"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"should use error codes >= 1000"}),", error codes in the range 0-999 are reserved by Centrifugo internal protocol. Error code field is ",(0,r.jsx)(n.code,{children:"uint32"})," internally."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsx)(n.p,{children:"Returning custom error does not apply to response on refresh request as there is no sense in returning an error (will not reach client anyway)."})}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-disconnect",children:"Return custom disconnect"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains a custom disconnect object to disconnect client in a custom way:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4000,\n "reconnect": false,\n "reason": "custom disconnect"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"must use numbers in the range 4000-4999 for custom disconnect codes"}),". Code is ",(0,r.jsx)(n.code,{children:"uint32"})," internally. Numbers below 4000 are reserved by Centrifugo internal protocol. Keep in mind that ",(0,r.jsx)(n.strong,{children:"due to WebSocket protocol limitations and Centrifugo internal protocol needs you need to keep disconnect reason string no longer than 32 symbols"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Returning custom disconnect does not apply to response on refresh request as there is no way to control disconnect at moment - the client will always be disconnected with ",(0,r.jsx)(n.code,{children:"expired"})," disconnect reason."]})}),"\n",(0,r.jsx)(n.h2,{id:"grpc-proxy",children:"GRPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can also proxy connection events to your backend over GRPC instead of HTTP. In this case, Centrifugo acts as a GRPC client and your backend acts as a GRPC server."}),"\n",(0,r.jsxs)(n.p,{children:["GRPC service definitions can be found in the Centrifugo repository: ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/blob/master/internal/proxyproto/proxy.proto",children:"proxy.proto"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"GRPC proxy inherits all the fields for HTTP proxy \u2013 so you can refer to field descriptions for HTTP above. Both proxy types in Centrifugo share the same Protobuf schema definitions."})}),"\n",(0,r.jsx)(n.p,{children:"Every proxy call in this case is a unary GRPC call. Centrifugo puts client headers into GRPC metadata (since GRPC doesn't have headers concept)."}),"\n",(0,r.jsxs)(n.p,{children:["All you need to do to enable proxying over GRPC instead of HTTP is to use ",(0,r.jsx)(n.code,{children:"grpc"})," schema in endpoint, for example for the connect proxy:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_connect_endpoint": "grpc://localhost:12000",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_refresh_endpoint": "grpc://localhost:12000",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or for RPC proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_rpc_endpoint": "grpc://localhost:12000",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["For publish proxy in namespace ",(0,r.jsx)(n.code,{children:"chat"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_publish_endpoint": "grpc://localhost:12000",\n "proxy_publish_timeout": "1s"\n "namespaces": [\n {\n "name": "chat",\n "publish": true,\n "proxy_publish": true\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Use subscribe proxy for all channels without namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_subscribe_endpoint": "grpc://localhost:12000",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"So the same as for HTTP, just the different endpoint scheme."}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-options",children:"GRPC proxy options"}),"\n",(0,r.jsx)(n.p,{children:"Some additional options exist to control GRPC proxy behavior."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_cert_file",children:"proxy_grpc_cert_file"}),"\n",(0,r.jsxs)(n.p,{children:["String, default: ",(0,r.jsx)(n.code,{children:'""'}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_key",children:"proxy_grpc_credentials_key"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsx)(n.p,{children:"Add custom key to per-RPC credentials."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_value",children:"proxy_grpc_credentials_value"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsxs)(n.p,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"proxy_grpc_credentials_key"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-example",children:"GRPC proxy example"}),"\n",(0,r.jsxs)(n.p,{children:["We have ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/proxy/grpc",children:"an example of backend server"})," (written in Go language) which can react to events from Centrifugo over GRPC. For other programming languages the approach is similar, i.e.:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Copy proxy Protobuf definitions"}),"\n",(0,r.jsx)(n.li,{children:"Generate GRPC code"}),"\n",(0,r.jsx)(n.li,{children:"Run backend service with you custom business logic"}),"\n",(0,r.jsx)(n.li,{children:"Point Centrifugo to it."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"header-proxy-rules",children:"Header proxy rules"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo not only supports HTTP-based client transports but also GRPC-based (for example GRPC unidirectional stream). Here is a table with rules used to proxy headers/metadata in various scenarios:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Client protocol type"}),(0,r.jsx)(n.th,{children:"Proxy type"}),(0,r.jsx)(n.th,{children:"Client headers"}),(0,r.jsx)(n.th,{children:"Client metadata"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"In proxy request headers"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request headers"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"binary-mode",children:"Binary mode"}),"\n",(0,r.jsxs)(n.p,{children:["As you may noticed there are several fields in request/result description of various proxy calls which use ",(0,r.jsx)(n.code,{children:"base64"})," encoding."]}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can work with binary Protobuf protocol (in case of bidirectional WebSocket transport). All our bidirectional clients support this."}),"\n",(0,r.jsx)(n.p,{children:"Most Centrifugo users use JSON for custom payloads: i.e. for data sent to a channel, for connection info attached while authenticating (which becomes part of presence response, join/leave messages and added to Publication client info when message published from a client side)."}),"\n",(0,r.jsx)(n.p,{children:"But since HTTP proxy works with JSON format (i.e. sends requests with JSON body) \u2013 it can not properly pass binary data to application backend. Arbitrary binary data can't be encoded into JSON."}),"\n",(0,r.jsx)(n.p,{children:"In this case it's possible to turn Centrifugo proxy into binary mode by using:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_binary_encoding": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Once enabled this option tells Centrifugo to use base64 format in requests and utilize fields like ",(0,r.jsx)(n.code,{children:"b64data"}),", ",(0,r.jsx)(n.code,{children:"b64info"})," with payloads encoded to base64 instead of their JSON field analogues."]}),"\n",(0,r.jsx)(n.p,{children:"While this feature is useful for HTTP proxy it's not really required if you are using GRPC proxy \u2013 since GRPC allows passing binary data just fine."}),"\n",(0,r.jsx)(n.p,{children:"Regarding b64 fields in proxy results \u2013 just use base64 fields when required \u2013 Centrifugo is smart enough to detect that you are using base64 field and will pick payload from it, decode from base64 automatically and will pass further to connections in binary format."}),"\n",(0,r.jsx)(n.h2,{id:"granular-proxy-mode",children:"Granular proxy mode"}),"\n",(0,r.jsx)(n.p,{children:"New in Centrifugo v3.1.0."}),"\n",(0,r.jsx)(n.p,{children:"By default, with proxy configuration shown above, you can only define a global proxy settings and one endpoint for each type of proxy (i.e. one for connect proxy, one for subscribe proxy, and so on). Also, you can configure only one set of headers to proxy which will be used by each proxy type. This may be sufficient for many use cases, but what if you need a more granular control? For example, use different subscribe proxy endpoints for different channel namespaces (i.e. when using microservice architecture)."}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo v3.1.0 introduced a new mode for proxy configuration called granular proxy mode. In this mode it's possible to configure subscribe and publish proxy behaviour on per-namespace level, use different set of headers passed to the proxy endpoint in each proxy type. Also, Centrifugo v3.1.0 introduced a concept of rpc namespaces (in addition to channel namespaces) \u2013 together with granular proxy mode this allows configuring rpc proxies on per rpc namespace basis."}),"\n",(0,r.jsx)(n.h3,{id:"enable-granular-proxy-mode",children:"Enable granular proxy mode"}),"\n",(0,r.jsxs)(n.p,{children:["Since the change is rather radical it requires a separate boolean option ",(0,r.jsx)(n.code,{children:"granular_proxy_mode"})," to be enabled. As soon as this option set Centrifugo does not use proxy configuration rules described above and follows the rules described below."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"defining-a-list-of-proxies",children:"Defining a list of proxies"}),"\n",(0,r.jsxs)(n.p,{children:["When using granular proxy mode on configuration top level you can define ",(0,r.jsx)(n.code,{children:'"proxies"'})," array with a list of different proxy objects. Each proxy object in an array should have at least two required fields: ",(0,r.jsx)(n.code,{children:"name"})," and ",(0,r.jsx)(n.code,{children:"endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Here is an example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [\n {\n "name": "connect",\n "endpoint": "http://localhost:3000/centrifugo/connect",\n "timeout": "500ms",\n "http_headers": ["Cookie"]\n },\n {\n "name": "refresh",\n "endpoint": "http://localhost:3000/centrifugo/refresh",\n "timeout": "500ms"\n },\n {\n "name": "subscribe1",\n "endpoint": "http://localhost:3001/centrifugo/subscribe"\n },\n {\n "name": "publish1",\n "endpoint": "http://localhost:3001/centrifugo/publish"\n },\n {\n "name": "rpc1",\n "endpoint": "http://localhost:3001/centrifugo/rpc"\n },\n {\n "name": "subscribe2",\n "endpoint": "http://localhost:3002/centrifugo/subscribe"\n },\n {\n "name": "publish2",\n "endpoint": "grpc://localhost:3002"\n }\n {\n "name": "rpc2",\n "endpoint": "grpc://localhost:3002"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Let's look at all fields for a proxy object which is possible to set for each proxy inside ",(0,r.jsx)(n.code,{children:'"proxies"'})," array."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field name"}),(0,r.jsx)(n.th,{children:"Field type"}),(0,r.jsx)(n.th,{children:"Required"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["Unique name of proxy used for referencing in configuration, must match regexp ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"endpoint"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["HTTP or GRPC endpoint in the same format as in default proxy mode. For example, ",(0,r.jsx)(n.code,{children:"http://localhost:3000/path"})," for HTTP or ",(0,r.jsx)(n.code,{children:"grpc://localhost:3000"})," for GRPC."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"timeout"}),(0,r.jsx)(n.td,{children:"duration (string)"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["Proxy request timeout, default ",(0,r.jsx)(n.code,{children:'"1s"'})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"http_headers"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of headers to proxy, by default no headers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_metadata"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of GRPC metadata keys to proxy, by default no metadata keys"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"binary_encoding"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Use base64 for payloads"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"include_connection_meta"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Include meta information (attached on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_cert_file"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_key"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Add custom key to per-RPC credentials."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_value"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"grpc_credentials_key"}),"."]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"granular-connect-and-refresh",children:"Granular connect and refresh"}),"\n",(0,r.jsx)(n.p,{children:"As soon as you defined a list of proxies you can reference them by a name to use a specific proxy configuration for a specific event."}),"\n",(0,r.jsx)(n.p,{children:"To enable connect proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["We have an ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/nodejs_granular_proxy",children:"example of Centrifugo integration with NodeJS"})," which uses granular proxy mode. Even if you are not using NodeJS on a backend an example can help you understand the idea."]}),"\n",(0,r.jsx)(n.p,{children:"Let's also add refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect",\n "refresh_proxy_name": "refresh"\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"granular-subscribe-and-publish",children:"Granular subscribe and publish"}),"\n",(0,r.jsxs)(n.p,{children:["Subscribe and publish proxy work per-namespace. This means that ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," are just a channel namespace options. So it's possible to define these options on configuration top-level (for channels in default top-level namespace) or inside namespace object."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [\n {\n "name": "ns1",\n "subscribe_proxy_name": "subscribe1",\n "publish": true,\n "publish_proxy_name": "publish1"\n },\n {\n "name": "ns2",\n "subscribe_proxy_name": "subscribe2",\n "publish": true,\n "publish_proxy_name": "publish2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," is empty then no subscribe proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," is empty then no publish proxy will be used for a namespace."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["You can define ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," on configuration top level \u2013 and in this case publish and subscribe requests for channels without explicit namespace will be proxied using this proxy. The same mechanics as for other channel options in Centrifugo."]})}),"\n",(0,r.jsx)(n.h3,{id:"granular-rpc",children:"Granular RPC"}),"\n",(0,r.jsx)(n.p,{children:"Analogous to channel namespaces it's possible to configure rpc namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [...],\n "rpc_namespaces": [\n {\n "name": "rpc_ns1",\n "rpc_proxy_name": "rpc1",\n },\n {\n "name": "rpc_ns2",\n "rpc_proxy_name": "rpc2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The mechanics is the same as for channel namespaces. RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns1:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc1"}),", RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns2:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc2"}),". So Centrifugo uses ",(0,r.jsx)(n.code,{children:":"})," as RPC namespace boundary in RPC method (just like it does for channel namespaces)."]}),"\n",(0,r.jsxs)(n.p,{children:["Just like channel namespaces RPC namespaces should have a name which match ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})," regexp pattern \u2013 this is validated on Centrifugo start."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["The same as for channel namespaces and channel options you can define ",(0,r.jsx)(n.code,{children:"rpc_proxy_name"})," on configuration top level \u2013 and in this case RPC calls without explicit namespace in RPC method will be proxied using this proxy."]})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},40330:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_connect_proxy-4318d8beb2c7553d9b30b2ed7fb8edac.png"},28719:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_publish_proxy-66ccb1e8b37ed8912d218b4529597bd9.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>d,a:()=>o});var r=s(67294);const i={},t=r.createContext(i);function o(e){const n=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9727],{21363:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>d,toc:()=>l});var r=s(85893),i=s(11151);const t={id:"proxy",title:"Proxy to backend"},o=void 0,d={id:"server/proxy",title:"Proxy to backend",description:"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.",source:"@site/versioned_docs/version-3/server/proxy.md",sourceDirName:"server",slug:"/server/proxy",permalink:"/docs/3/server/proxy",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/server/proxy.md",tags:[],version:"3",frontMatter:{id:"proxy",title:"Proxy to backend"},sidebar:"Guides",previous:{title:"Engines, scalability",permalink:"/docs/3/server/engines"},next:{title:"History and recovery",permalink:"/docs/3/server/history_and_recovery"}},c={},l=[{value:"HTTP proxy",id:"http-proxy",level:2},{value:"HTTP request structure",id:"http-request-structure",level:3},{value:"Proxy HTTP headers",id:"proxy-http-headers",level:3},{value:"Proxy GRPC metadata",id:"proxy-grpc-metadata",level:3},{value:"Connect proxy",id:"connect-proxy",level:3},{value:"Connect request fields",id:"connect-request-fields",level:4},{value:"Connect result fields",id:"connect-result-fields",level:4},{value:"Options",id:"options",level:4},{value:"Example",id:"example",level:4},{value:"Refresh proxy",id:"refresh-proxy",level:3},{value:"Refresh request fields",id:"refresh-request-fields",level:4},{value:"Refresh result fields",id:"refresh-result-fields",level:4},{value:"Options",id:"options-1",level:4},{value:"RPC proxy",id:"rpc-proxy",level:3},{value:"RPC request fields",id:"rpc-request-fields",level:4},{value:"RPC result fields",id:"rpc-result-fields",level:4},{value:"Options",id:"options-2",level:4},{value:"Subscribe proxy",id:"subscribe-proxy",level:3},{value:"Subscribe request fields",id:"subscribe-request-fields",level:4},{value:"Subscribe result fields",id:"subscribe-result-fields",level:4},{value:"Override object",id:"override-object",level:4},{value:"Options",id:"options-3",level:4},{value:"Publish proxy",id:"publish-proxy",level:3},{value:"Publish request fields",id:"publish-request-fields",level:4},{value:"Publish result fields",id:"publish-result-fields",level:4},{value:"Options",id:"options-4",level:4},{value:"Return custom error",id:"return-custom-error",level:3},{value:"Return custom disconnect",id:"return-custom-disconnect",level:3},{value:"GRPC proxy",id:"grpc-proxy",level:2},{value:"GRPC proxy options",id:"grpc-proxy-options",level:3},{value:"proxy_grpc_cert_file",id:"proxy_grpc_cert_file",level:4},{value:"proxy_grpc_credentials_key",id:"proxy_grpc_credentials_key",level:4},{value:"proxy_grpc_credentials_value",id:"proxy_grpc_credentials_value",level:4},{value:"GRPC proxy example",id:"grpc-proxy-example",level:3},{value:"Header proxy rules",id:"header-proxy-rules",level:2},{value:"Binary mode",id:"binary-mode",level:2},{value:"Granular proxy mode",id:"granular-proxy-mode",level:2},{value:"Enable granular proxy mode",id:"enable-granular-proxy-mode",level:3},{value:"Defining a list of proxies",id:"defining-a-list-of-proxies",level:3},{value:"Granular connect and refresh",id:"granular-connect-and-refresh",level:3},{value:"Granular subscribe and publish",id:"granular-subscribe-and-publish",level:3},{value:"Granular RPC",id:"granular-rpc",level:3}];function a(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.p,{children:"The list of events that can be proxied:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Connect \u2013 called when a client connects to Centrifugo, so it's possible to authenticate user, return custom data to a client, subscribe connection to several channels, attach meta information to the connection, and so on. Works for bidirectional and unidirectional transports."}),"\n",(0,r.jsx)(n.li,{children:"Refresh - called when a client session is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical connection liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."}),"\n",(0,r.jsx)(n.li,{children:"Subscribe - called when clients try to subscribe on a channel, so it's possible to check permissions and return custom initial subscription data. Works for bidirectional transports only."}),"\n",(0,r.jsx)(n.li,{children:"Publish - called when a client tries to publish into a channel, so it's possible to check permissions and optionally modify publication data. Works for bidirectional transports only."}),"\n",(0,r.jsx)(n.li,{children:"RPC - called when a client sends RPC, you can do whatever logic you need based on a client-provided RPC method and params. Works for bidirectional transports only."}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"At the moment Centrifugo can proxy these events over two protocols:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"HTTP (JSON payloads)"}),"\n",(0,r.jsx)(n.li,{children:"GRPC (Protobuf messages)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"http-proxy",children:"HTTP proxy"}),"\n",(0,r.jsx)(n.p,{children:"HTTP proxy in Centrifugo converts client connection events into HTTP call to the application backend."}),"\n",(0,r.jsx)(n.h3,{id:"http-request-structure",children:"HTTP request structure"}),"\n",(0,r.jsxs)(n.p,{children:["All proxy calls are ",(0,r.jsx)(n.strong,{children:"HTTP POST"})," requests that will be sent from Centrifugo to configured endpoints with a configured timeout. These requests will have some headers copied from the original client request (see details below) and include JSON body which varies depending on call type (for example data sent by a client in RPC call etc, see more details about JSON bodies below)."]}),"\n",(0,r.jsx)(n.h3,{id:"proxy-http-headers",children:"Proxy HTTP headers"}),"\n",(0,r.jsx)(n.p,{children:"The good thing about Centrifugo HTTP proxy is that it transparently proxies original HTTP request headers in a request to app backend. In most cases this allows achieving transparent authentication on the application backend side. But it's required to provide an explicit list of HTTP headers you want to be proxied, for example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Alternatively, you can set a list of headers via an environment variable (space separated):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'export CENTRIFUGO_PROXY_HTTP_HEADERS="Cookie User-Agent X-B3-TraceId X-B3-SpanId" ./centrifugo\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Centrifugo forces the",(0,r.jsx)(n.code,{children:" Content-Type"})," header to be ",(0,r.jsx)(n.code,{children:"application/json"})," in all HTTP proxy requests since it sends the body in JSON format to the application backend."]})}),"\n",(0,r.jsx)(n.h3,{id:"proxy-grpc-metadata",children:"Proxy GRPC metadata"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.a,{href:"/docs/3/transports/uni_grpc",children:"GRPC unidirectional stream"})," is used as a client transport then you may want to proxy GRPC metadata from the client request. In this case you may configure ",(0,r.jsx)(n.code,{children:"proxy_grpc_metadata"})," option. This is an array of string metadata keys which will be proxied. These metadata keys transformed to HTTP headers of proxy request. By default no metadata keys are proxied."]}),"\n",(0,r.jsxs)(n.p,{children:["See below ",(0,r.jsx)(n.a,{href:"#header-proxy-rules",children:"the table of rules"})," how metadata and headers proxied in transport/proxy different scenarios."]}),"\n",(0,r.jsx)(n.h3,{id:"connect-proxy",children:"Connect proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_connect_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 connection requests ",(0,r.jsx)(n.strong,{children:"without JWT set"})," will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," URL endpoint. On your backend side, you can authenticate the incoming connection and return client credentials to Centrifugo in response to the proxied request."]}),"\n",(0,r.jsx)(n.admonition,{type:"danger",children:(0,r.jsxs)(n.p,{children:["Make sure you properly configured ",(0,r.jsx)(n.a,{href:"/docs/3/server/configuration#allowed_origins",children:"allowed_origins"})," Centrifugo option or check request origin on your backend side upon receiving connect request from Centrifugo. Otherwise, your site can be vulnerable to CSRF attacks if you are using WebSocket transport for client connections."]})}),"\n",(0,r.jsxs)(n.p,{children:["Yes, this means you don't need to generate JWT and pass it to a client-side and can rely on a cookie while authenticating the user. ",(0,r.jsx)(n.strong,{children:"Centrifugo should work on the same domain in this case so your site cookie could be passed to Centrifugo by browsers"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you want to pass some custom authentication token from a client side (not in Centrifugo JWT format) but force request to be proxied then you may put it in a cookie or use connection request custom ",(0,r.jsx)(n.code,{children:"data"})," field (available in all our transports). This ",(0,r.jsx)(n.code,{children:"data"})," can contain arbitrary payload you want to pass from a client to a server."]})}),"\n",(0,r.jsxs)(n.p,{children:["This also means that ",(0,r.jsx)(n.strong,{children:"every"})," new connection from a user will result in an HTTP POST request to your application backend. While with JWT token you usually generate it once on application page reload, if client reconnects due to Centrifugo restart or internet connection loss it uses the same JWT it had before thus usually no additional requests are generated during reconnect process (until JWT expired)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(70160).Z+"",width:"2600",height:"1032"})}),"\n",(0,r.jsxs)(n.p,{children:["Payload example that will be sent to app backend when client without token wants to establish a connection with Centrifugo and ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," is set to non-empty URL string:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"user": "56"}}\n'})}),"\n",(0,r.jsx)(n.p,{children:"This response allows connecting and tells Centrifugo the ID of a user. See below the full list of supported fields in the result."}),"\n",(0,r.jsx)(n.p,{children:"Several app examples which use connect proxy can be found in our blog:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"With NodeJS"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/11/04/integrating-with-django-building-chat-application",children:"With Django"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",children:"With Laravel"})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"connect-request-fields",children:"Connect request fields"}),"\n",(0,r.jsx)(n.p,{children:"This is what sent from Centrifugo to application backend in case of connect proxy request."}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional name of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"version"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional version of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from the client in base64 format (if the binary proxy mode is used)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"Array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"list of server-side channels client want to subscribe to, the application server must check permissions and add allowed channels to result"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"connect-result-fields",children:"Connect result fields"}),"\n",(0,r.jsxs)(n.p,{children:["This is what application returns to Centrifugo inside ",(0,r.jsx)(n.code,{children:"result"})," field in case of connect proxy request."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"user ID (calculated on app backend based on request cookie header for example). Return it as an empty string for accepting unauthenticated requests"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a timestamp when connection must be considered expired. If not set or set to ",(0,r.jsx)(n.code,{children:"0"})," connection won't expire at all"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in connect command response."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in the connect command response for binary connections, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["allows providing a list of server-side channels to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"subs"}),(0,r.jsx)(n.td,{children:"map of SubscribeOptions"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["map of channels with options to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsxs)(n.td,{children:["JSON object (ex. ",(0,r.jsx)(n.code,{children:'{"key": "value"}'}),")"]}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a custom data to attach to connection (this ",(0,r.jsx)(n.strong,{children:"won't be exposed to client-side"}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_connect_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h4,{id:"example",children:"Example"}),"\n",(0,r.jsxs)(n.p,{children:["Here is the simplest example of the connect handler in Tornado Python framework (note that in a real system you need to authenticate the user on your backend side, here we just return ",(0,r.jsx)(n.code,{children:'"56"'})," as user ID):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"class CentrifugoConnectHandler(tornado.web.RequestHandler):\n\n def check_xsrf_cookie(self):\n pass\n\n def post(self):\n self.set_header('Content-Type', 'application/json; charset=\"utf-8\"')\n data = json.dumps({\n 'result': {\n 'user': '56'\n }\n })\n self.write(data)\n\n\ndef main():\n options.parse_command_line()\n app = tornado.web.Application([\n (r'/centrifugo/connect', CentrifugoConnectHandler),\n ])\n app.listen(3000)\n tornado.ioloop.IOLoop.instance().start()\n\n\nif __name__ == '__main__':\n main()\n"})}),"\n",(0,r.jsx)(n.p,{children:"This example should help you to implement a similar HTTP handler in any language/framework you are using on the backend side."}),"\n",(0,r.jsxs)(n.p,{children:["We also have a tutorial in the blog about ",(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"Centrifugo integration with NodeJS"})," which uses connect proxy and native session middleware of Express.js to authenticate connections. Even if you are not using NodeJS on a backend a tutorial can help you understand the idea."]}),"\n",(0,r.jsx)(n.h3,{id:"refresh-proxy",children:"Refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_refresh_endpoint": "http://localhost:3000/centrifugo/refresh",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_refresh_endpoint"})," when it's time to refresh the connection. Centrifugo itself will ask your backend about connection validity instead of refresh workflow on the client-side."]}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in refresh request (when the connection is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"refresh-request-fields",children:"Refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"refresh-result-fields",children:"Refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-1",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_refresh_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"rpc-proxy",children:"RPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_rpc_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["RPC calls over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_rpc_endpoint"}),". This allows a developer to utilize WebSocket (or SockJS) connection in a bidirectional way."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in RPC request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "method": "getCurrentPrice",\n "data":{"params": {"object_id": 12}}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"data": {"answer": "2019"}}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"rpc-request-fields",children:"RPC request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"method"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"an RPC method string, if the client does not use named RPC call then method will be omitted"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC custom data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"rpc-result-fields",children:"RPC result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC response - any valid JSON is supported"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["can be set instead of ",(0,r.jsx)(n.code,{children:"data"})," for binary response encoded in base64 format"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-2",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_rpc_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return a custom error."}),"\n",(0,r.jsx)(n.h3,{id:"subscribe-proxy",children:"Subscribe proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 subscribe requests sent over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_subscribe_endpoint"}),". This allows you to check the access of the client to a channel."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Subscribe proxy does not proxy ",(0,r.jsx)(n.a,{href:"/docs/3/server/channels#private-channel-prefix",children:"private"})," and ",(0,r.jsx)(n.a,{href:"/docs/3/server/channels#user-channel-boundary",children:"user-limited"})," channels at the moment"]}),". That's because those are already providing a level of security (user-limited channels check current user ID, private channels require subscription token). In some cases you may use subscribe proxy as a replacement for private channels actually: if you prefer to check permissions using the proxy to backend mechanism \u2013 just stop using ",(0,r.jsx)(n.code,{children:"$"})," prefixes in channels, properly configure subscribe proxy and validate subscriptions upon proxy from Centrifugo to your backend (issued each time user tries to subscribe on a channel for which subscribe proxy enabled)."]})}),"\n",(0,r.jsxs)(n.p,{children:["Unlike proxy types described above subscribe proxy must be enabled per channel namespace. This means that every namespace (including global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," that enables subscribe proxy for channels in a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable subscribe proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "proxy_subscribe": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in subscribe request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if subscription is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-request-fields",children:"Subscribe request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to subscribe to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"custom data from client sent with subscription request (this field will only be set if provided by a client on subscribe). Available since Centrifugo v3.1.1"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional subscription data from the client in base64 format (if the binary proxy mode is used). Available since Centrifugo v3.1.1"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-result-fields",children:"Subscribe result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary connection channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"override"}),(0,r.jsx)(n.td,{children:"Override object"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"presence"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override presence"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"position"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override position"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"recover"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow subscribing."}),"\n",(0,r.jsx)(n.h4,{id:"options-3",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_subscribe_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"publish-proxy",children:"Publish proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 publish calls sent by a client will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_publish_endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"This request happens BEFORE a message is published to a channel, so your backend can validate whether a client can publish data to a channel. An important thing here is that publication to the channel can fail after your backend successfully validated publish request (for example publish to Redis by Centrifugo returned an error). In this case, your backend won't know about the error that happened but this error will propagate to the client-side."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(97172).Z+"",width:"2600",height:"1098"})}),"\n",(0,r.jsxs)(n.p,{children:["Like the subscribe proxy, publish proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_publish"})," that enables publish proxy for channels in the namespace. All other namespace options will be taken into account before making a proxy request, so you also need to turn on the ",(0,r.jsx)(n.code,{children:"publish"})," option too."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable publish proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_publish"})," and ",(0,r.jsx)(n.code,{children:"publish"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "publish": true,\n "proxy_publish": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "publish": true,\n "proxy_publish": true\n }]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Keep in mind that this will only work if the ",(0,r.jsx)(n.code,{children:"publish"})," channel option is on for a channel namespace (or for a global top-level namespace)."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in a publish request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index",\n "data":{"input":"hello"}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if publish is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"publish-request-fields",children:"Publish request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to publish to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"publish-result-fields",children:"Publish result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["an optional JSON data to send into a channel ",(0,r.jsx)(n.strong,{children:"instead of"})," original data sent by a client"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary data encoded in base64 format, the meaning is the same as for data above, will be decoded to raw bytes on Centrifugo side before publishing"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"skip_history"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["when set to ",(0,r.jsx)(n.code,{children:"true"})," Centrifugo won't save publication to the channel history"]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow publishing."}),"\n",(0,r.jsx)(n.h4,{id:"options-4",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_publish_timeout"})," (float, in seconds) config option controls timeout of HTTP POST request sent to app backend."]}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-error",children:"Return custom error"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains an error to return it to the client:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 1000,\n "message": "custom error"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"should use error codes >= 1000"}),", error codes in the range 0-999 are reserved by Centrifugo internal protocol. Error code field is ",(0,r.jsx)(n.code,{children:"uint32"})," internally."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsx)(n.p,{children:"Returning custom error does not apply to response on refresh request as there is no sense in returning an error (will not reach client anyway)."})}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-disconnect",children:"Return custom disconnect"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains a custom disconnect object to disconnect client in a custom way:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4000,\n "reconnect": false,\n "reason": "custom disconnect"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"must use numbers in the range 4000-4999 for custom disconnect codes"}),". Code is ",(0,r.jsx)(n.code,{children:"uint32"})," internally. Numbers below 4000 are reserved by Centrifugo internal protocol. Keep in mind that ",(0,r.jsx)(n.strong,{children:"due to WebSocket protocol limitations and Centrifugo internal protocol needs you need to keep disconnect reason string no longer than 32 symbols"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Returning custom disconnect does not apply to response on refresh request as there is no way to control disconnect at moment - the client will always be disconnected with ",(0,r.jsx)(n.code,{children:"expired"})," disconnect reason."]})}),"\n",(0,r.jsx)(n.h2,{id:"grpc-proxy",children:"GRPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can also proxy connection events to your backend over GRPC instead of HTTP. In this case, Centrifugo acts as a GRPC client and your backend acts as a GRPC server."}),"\n",(0,r.jsxs)(n.p,{children:["GRPC service definitions can be found in the Centrifugo repository: ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/blob/master/internal/proxyproto/proxy.proto",children:"proxy.proto"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"GRPC proxy inherits all the fields for HTTP proxy \u2013 so you can refer to field descriptions for HTTP above. Both proxy types in Centrifugo share the same Protobuf schema definitions."})}),"\n",(0,r.jsx)(n.p,{children:"Every proxy call in this case is a unary GRPC call. Centrifugo puts client headers into GRPC metadata (since GRPC doesn't have headers concept)."}),"\n",(0,r.jsxs)(n.p,{children:["All you need to do to enable proxying over GRPC instead of HTTP is to use ",(0,r.jsx)(n.code,{children:"grpc"})," schema in endpoint, for example for the connect proxy:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_connect_endpoint": "grpc://localhost:12000",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_refresh_endpoint": "grpc://localhost:12000",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or for RPC proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_rpc_endpoint": "grpc://localhost:12000",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["For publish proxy in namespace ",(0,r.jsx)(n.code,{children:"chat"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_publish_endpoint": "grpc://localhost:12000",\n "proxy_publish_timeout": "1s"\n "namespaces": [\n {\n "name": "chat",\n "publish": true,\n "proxy_publish": true\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Use subscribe proxy for all channels without namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_subscribe_endpoint": "grpc://localhost:12000",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"So the same as for HTTP, just the different endpoint scheme."}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-options",children:"GRPC proxy options"}),"\n",(0,r.jsx)(n.p,{children:"Some additional options exist to control GRPC proxy behavior."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_cert_file",children:"proxy_grpc_cert_file"}),"\n",(0,r.jsxs)(n.p,{children:["String, default: ",(0,r.jsx)(n.code,{children:'""'}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_key",children:"proxy_grpc_credentials_key"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsx)(n.p,{children:"Add custom key to per-RPC credentials."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_value",children:"proxy_grpc_credentials_value"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsxs)(n.p,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"proxy_grpc_credentials_key"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-example",children:"GRPC proxy example"}),"\n",(0,r.jsxs)(n.p,{children:["We have ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/proxy/grpc",children:"an example of backend server"})," (written in Go language) which can react to events from Centrifugo over GRPC. For other programming languages the approach is similar, i.e.:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Copy proxy Protobuf definitions"}),"\n",(0,r.jsx)(n.li,{children:"Generate GRPC code"}),"\n",(0,r.jsx)(n.li,{children:"Run backend service with you custom business logic"}),"\n",(0,r.jsx)(n.li,{children:"Point Centrifugo to it."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"header-proxy-rules",children:"Header proxy rules"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo not only supports HTTP-based client transports but also GRPC-based (for example GRPC unidirectional stream). Here is a table with rules used to proxy headers/metadata in various scenarios:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Client protocol type"}),(0,r.jsx)(n.th,{children:"Proxy type"}),(0,r.jsx)(n.th,{children:"Client headers"}),(0,r.jsx)(n.th,{children:"Client metadata"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"In proxy request headers"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request headers"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"binary-mode",children:"Binary mode"}),"\n",(0,r.jsxs)(n.p,{children:["As you may noticed there are several fields in request/result description of various proxy calls which use ",(0,r.jsx)(n.code,{children:"base64"})," encoding."]}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can work with binary Protobuf protocol (in case of bidirectional WebSocket transport). All our bidirectional clients support this."}),"\n",(0,r.jsx)(n.p,{children:"Most Centrifugo users use JSON for custom payloads: i.e. for data sent to a channel, for connection info attached while authenticating (which becomes part of presence response, join/leave messages and added to Publication client info when message published from a client side)."}),"\n",(0,r.jsx)(n.p,{children:"But since HTTP proxy works with JSON format (i.e. sends requests with JSON body) \u2013 it can not properly pass binary data to application backend. Arbitrary binary data can't be encoded into JSON."}),"\n",(0,r.jsx)(n.p,{children:"In this case it's possible to turn Centrifugo proxy into binary mode by using:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_binary_encoding": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Once enabled this option tells Centrifugo to use base64 format in requests and utilize fields like ",(0,r.jsx)(n.code,{children:"b64data"}),", ",(0,r.jsx)(n.code,{children:"b64info"})," with payloads encoded to base64 instead of their JSON field analogues."]}),"\n",(0,r.jsx)(n.p,{children:"While this feature is useful for HTTP proxy it's not really required if you are using GRPC proxy \u2013 since GRPC allows passing binary data just fine."}),"\n",(0,r.jsx)(n.p,{children:"Regarding b64 fields in proxy results \u2013 just use base64 fields when required \u2013 Centrifugo is smart enough to detect that you are using base64 field and will pick payload from it, decode from base64 automatically and will pass further to connections in binary format."}),"\n",(0,r.jsx)(n.h2,{id:"granular-proxy-mode",children:"Granular proxy mode"}),"\n",(0,r.jsx)(n.p,{children:"New in Centrifugo v3.1.0."}),"\n",(0,r.jsx)(n.p,{children:"By default, with proxy configuration shown above, you can only define a global proxy settings and one endpoint for each type of proxy (i.e. one for connect proxy, one for subscribe proxy, and so on). Also, you can configure only one set of headers to proxy which will be used by each proxy type. This may be sufficient for many use cases, but what if you need a more granular control? For example, use different subscribe proxy endpoints for different channel namespaces (i.e. when using microservice architecture)."}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo v3.1.0 introduced a new mode for proxy configuration called granular proxy mode. In this mode it's possible to configure subscribe and publish proxy behaviour on per-namespace level, use different set of headers passed to the proxy endpoint in each proxy type. Also, Centrifugo v3.1.0 introduced a concept of rpc namespaces (in addition to channel namespaces) \u2013 together with granular proxy mode this allows configuring rpc proxies on per rpc namespace basis."}),"\n",(0,r.jsx)(n.h3,{id:"enable-granular-proxy-mode",children:"Enable granular proxy mode"}),"\n",(0,r.jsxs)(n.p,{children:["Since the change is rather radical it requires a separate boolean option ",(0,r.jsx)(n.code,{children:"granular_proxy_mode"})," to be enabled. As soon as this option set Centrifugo does not use proxy configuration rules described above and follows the rules described below."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"defining-a-list-of-proxies",children:"Defining a list of proxies"}),"\n",(0,r.jsxs)(n.p,{children:["When using granular proxy mode on configuration top level you can define ",(0,r.jsx)(n.code,{children:'"proxies"'})," array with a list of different proxy objects. Each proxy object in an array should have at least two required fields: ",(0,r.jsx)(n.code,{children:"name"})," and ",(0,r.jsx)(n.code,{children:"endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Here is an example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [\n {\n "name": "connect",\n "endpoint": "http://localhost:3000/centrifugo/connect",\n "timeout": "500ms",\n "http_headers": ["Cookie"]\n },\n {\n "name": "refresh",\n "endpoint": "http://localhost:3000/centrifugo/refresh",\n "timeout": "500ms"\n },\n {\n "name": "subscribe1",\n "endpoint": "http://localhost:3001/centrifugo/subscribe"\n },\n {\n "name": "publish1",\n "endpoint": "http://localhost:3001/centrifugo/publish"\n },\n {\n "name": "rpc1",\n "endpoint": "http://localhost:3001/centrifugo/rpc"\n },\n {\n "name": "subscribe2",\n "endpoint": "http://localhost:3002/centrifugo/subscribe"\n },\n {\n "name": "publish2",\n "endpoint": "grpc://localhost:3002"\n }\n {\n "name": "rpc2",\n "endpoint": "grpc://localhost:3002"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Let's look at all fields for a proxy object which is possible to set for each proxy inside ",(0,r.jsx)(n.code,{children:'"proxies"'})," array."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field name"}),(0,r.jsx)(n.th,{children:"Field type"}),(0,r.jsx)(n.th,{children:"Required"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["Unique name of proxy used for referencing in configuration, must match regexp ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"endpoint"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["HTTP or GRPC endpoint in the same format as in default proxy mode. For example, ",(0,r.jsx)(n.code,{children:"http://localhost:3000/path"})," for HTTP or ",(0,r.jsx)(n.code,{children:"grpc://localhost:3000"})," for GRPC."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"timeout"}),(0,r.jsx)(n.td,{children:"duration (string)"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["Proxy request timeout, default ",(0,r.jsx)(n.code,{children:'"1s"'})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"http_headers"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of headers to proxy, by default no headers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_metadata"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of GRPC metadata keys to proxy, by default no metadata keys"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"binary_encoding"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Use base64 for payloads"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"include_connection_meta"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Include meta information (attached on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_cert_file"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_key"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Add custom key to per-RPC credentials."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_value"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"grpc_credentials_key"}),"."]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"granular-connect-and-refresh",children:"Granular connect and refresh"}),"\n",(0,r.jsx)(n.p,{children:"As soon as you defined a list of proxies you can reference them by a name to use a specific proxy configuration for a specific event."}),"\n",(0,r.jsx)(n.p,{children:"To enable connect proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["We have an ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/nodejs_granular_proxy",children:"example of Centrifugo integration with NodeJS"})," which uses granular proxy mode. Even if you are not using NodeJS on a backend an example can help you understand the idea."]}),"\n",(0,r.jsx)(n.p,{children:"Let's also add refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect",\n "refresh_proxy_name": "refresh"\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"granular-subscribe-and-publish",children:"Granular subscribe and publish"}),"\n",(0,r.jsxs)(n.p,{children:["Subscribe and publish proxy work per-namespace. This means that ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," are just a channel namespace options. So it's possible to define these options on configuration top-level (for channels in default top-level namespace) or inside namespace object."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [\n {\n "name": "ns1",\n "subscribe_proxy_name": "subscribe1",\n "publish": true,\n "publish_proxy_name": "publish1"\n },\n {\n "name": "ns2",\n "subscribe_proxy_name": "subscribe2",\n "publish": true,\n "publish_proxy_name": "publish2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," is empty then no subscribe proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," is empty then no publish proxy will be used for a namespace."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["You can define ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," on configuration top level \u2013 and in this case publish and subscribe requests for channels without explicit namespace will be proxied using this proxy. The same mechanics as for other channel options in Centrifugo."]})}),"\n",(0,r.jsx)(n.h3,{id:"granular-rpc",children:"Granular RPC"}),"\n",(0,r.jsx)(n.p,{children:"Analogous to channel namespaces it's possible to configure rpc namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [...],\n "rpc_namespaces": [\n {\n "name": "rpc_ns1",\n "rpc_proxy_name": "rpc1",\n },\n {\n "name": "rpc_ns2",\n "rpc_proxy_name": "rpc2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The mechanics is the same as for channel namespaces. RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns1:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc1"}),", RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns2:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc2"}),". So Centrifugo uses ",(0,r.jsx)(n.code,{children:":"})," as RPC namespace boundary in RPC method (just like it does for channel namespaces)."]}),"\n",(0,r.jsxs)(n.p,{children:["Just like channel namespaces RPC namespaces should have a name which match ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})," regexp pattern \u2013 this is validated on Centrifugo start."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["The same as for channel namespaces and channel options you can define ",(0,r.jsx)(n.code,{children:"rpc_proxy_name"})," on configuration top level \u2013 and in this case RPC calls without explicit namespace in RPC method will be proxied using this proxy."]})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},70160:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_connect_proxy-4318d8beb2c7553d9b30b2ed7fb8edac.png"},97172:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_publish_proxy-66ccb1e8b37ed8912d218b4529597bd9.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>d,a:()=>o});var r=s(67294);const i={},t=r.createContext(i);function o(e){const n=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1f391b9e.611ab5a0.js b/assets/js/1f391b9e.611ab5a0.js deleted file mode 100644 index 7057e9052..000000000 --- a/assets/js/1f391b9e.611ab5a0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3085],{29688:(e,a,s)=>{s.r(a),s.d(a,{default:()=>g});s(67294);var t=s(36905),d=s(62581),l=s(18015),i=s(78299),r=s(33658),n=s(95967),c=s(94007),o=s(18842);const m={mdxPageWrapper:"mdxPageWrapper_j9I6"};var p=s(85893);function g(e){const{content:a}=e,{metadata:{title:s,editUrl:g,description:x,frontMatter:h,unlisted:j,lastUpdatedBy:v,lastUpdatedAt:_},assets:u}=a,{keywords:f,wrapperClassName:k,hide_table_of_contents:Z}=h,w=u.image??h.image,N=!!(g||_||v);return(0,p.jsx)(d.FG,{className:(0,t.Z)(k??l.k.wrapper.mdxPages,l.k.page.mdxPage),children:(0,p.jsxs)(i.Z,{children:[(0,p.jsx)(d.d,{title:s,description:x,keywords:f,image:w}),(0,p.jsx)("main",{className:"container container--fluid margin-vert--lg",children:(0,p.jsxs)("div",{className:(0,t.Z)("row",m.mdxPageWrapper),children:[(0,p.jsxs)("div",{className:(0,t.Z)("col",!Z&&"col--8"),children:[j&&(0,p.jsx)(c.Z,{}),(0,p.jsx)("article",{children:(0,p.jsx)(r.Z,{children:(0,p.jsx)(a,{})})}),N&&(0,p.jsx)(o.Z,{className:(0,t.Z)("margin-top--sm",l.k.pages.pageFooterEditMetaRow),editUrl:g,lastUpdatedAt:_,lastUpdatedBy:v})]}),!Z&&a.toc.length>0&&(0,p.jsx)("div",{className:"col col--2",children:(0,p.jsx)(n.Z,{toc:a.toc,minHeadingLevel:h.toc_min_heading_level,maxHeadingLevel:h.toc_max_heading_level})})]})})]})})}}}]); \ No newline at end of file diff --git a/assets/js/1f391b9e.a3a4dbd2.js b/assets/js/1f391b9e.a3a4dbd2.js new file mode 100644 index 000000000..855482c7a --- /dev/null +++ b/assets/js/1f391b9e.a3a4dbd2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3085],{14247:(e,a,s)=>{s.r(a),s.d(a,{default:()=>g});s(67294);var t=s(36905),d=s(71667),l=s(35281),i=s(7372),r=s(95896),n=s(39407),c=s(22212),o=s(47265);const m={mdxPageWrapper:"mdxPageWrapper_j9I6"};var p=s(85893);function g(e){const{content:a}=e,{metadata:{title:s,editUrl:g,description:x,frontMatter:h,unlisted:j,lastUpdatedBy:v,lastUpdatedAt:_},assets:u}=a,{keywords:f,wrapperClassName:k,hide_table_of_contents:Z}=h,w=u.image??h.image,N=!!(g||_||v);return(0,p.jsx)(d.FG,{className:(0,t.Z)(k??l.k.wrapper.mdxPages,l.k.page.mdxPage),children:(0,p.jsxs)(i.Z,{children:[(0,p.jsx)(d.d,{title:s,description:x,keywords:f,image:w}),(0,p.jsx)("main",{className:"container container--fluid margin-vert--lg",children:(0,p.jsxs)("div",{className:(0,t.Z)("row",m.mdxPageWrapper),children:[(0,p.jsxs)("div",{className:(0,t.Z)("col",!Z&&"col--8"),children:[j&&(0,p.jsx)(c.Z,{}),(0,p.jsx)("article",{children:(0,p.jsx)(r.Z,{children:(0,p.jsx)(a,{})})}),N&&(0,p.jsx)(o.Z,{className:(0,t.Z)("margin-top--sm",l.k.pages.pageFooterEditMetaRow),editUrl:g,lastUpdatedAt:_,lastUpdatedBy:v})]}),!Z&&a.toc.length>0&&(0,p.jsx)("div",{className:"col col--2",children:(0,p.jsx)(n.Z,{toc:a.toc,minHeadingLevel:h.toc_min_heading_level,maxHeadingLevel:h.toc_max_heading_level})})]})})]})})}}}]); \ No newline at end of file diff --git a/assets/js/20c4d804.3c831ba1.js b/assets/js/20c4d804.9ada3512.js similarity index 99% rename from assets/js/20c4d804.3c831ba1.js rename to assets/js/20c4d804.9ada3512.js index a7265c6f7..b7b381db6 100644 --- a/assets/js/20c4d804.3c831ba1.js +++ b/assets/js/20c4d804.9ada3512.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7973],{94567:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>d});var r=n(85893),t=n(11151);const o={id:"scale",sidebar_label:"Scale to 100k room members",title:"Scale to 100k cats in room"},i=void 0,a={id:"tutorial/scale",title:"Scale to 100k cats in room",description:"Congratulations \u2013 we've built an awesome app and we are done with the development within this tutorial! \ud83c\udf89",source:"@site/docs/tutorial/scale.md",sourceDirName:"tutorial",slug:"/tutorial/scale",permalink:"/docs/tutorial/scale",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/scale.md",tags:[],version:"current",frontMatter:{id:"scale",sidebar_label:"Scale to 100k room members",title:"Scale to 100k cats in room"},sidebar:"Tutorial",previous:{title:"Broadcast: outbox and CDC",permalink:"/docs/tutorial/outbox_cdc"},next:{title:"Wrapping up \u2013 things learnt",permalink:"/docs/tutorial/outro"}},c={},d=[];function l(e){const s={a:"a",admonition:"admonition",code:"code",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.p,{children:"Congratulations \u2013 we've built an awesome app and we are done with the development within this tutorial! \ud83c\udf89"}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.img,{src:n(47692).Z+"",width:"2740",height:"1510"})}),"\n",(0,r.jsx)(s.p,{children:"But before wrapping up, let's experiment a little. Here we will try to look at some latency numbers for the room with 100, 1k, 10k, 100k members in different scenarios. Not many apps will reach 100k members in one group scale, but we want to show that Centrifugo gives you a way to grow this big keeping reasonable latency times, and also gives answers how to reduce latency and increase a system throughput further."}),"\n",(0,r.jsx)(s.admonition,{type:"info",children:(0,r.jsxs)(s.p,{children:["This chapter is still to be improved. We've included some numbers we were able to get while experimenting with the app \u2013 but left configuring Redis Engine out of scope for now. In experiments we used Centrifugo running outside of Docker. See how we did it in ",(0,r.jsx)(s.a,{href:"/docs/tutorial/tips_and_tricks#point-to-centrifugo-running-on-host-outside-docker",children:"Tips and Tricks"}),"."]})}),"\n",(0,r.jsxs)(s.p,{children:["In our blog post ",(0,r.jsx)(s.a,{href:"/blog/2020/02/10/million-connections-with-centrifugo",children:"Million connections with Centrifugo"})," we've shown that on a limited hardware resources, comparable to one modern server machine, delivering 500k messages per second with delivery latency no more than 200ms in 99 percentile is possible with Centrifugo."]}),"\n",(0,r.jsxs)(s.p,{children:["But this case is different, in this app we want to have large group chats with many members. The difference here is that publishing involves sending a message to each individual channel \u2013 so instead of small fan-in and large fan-out ",(0,r.jsx)(s.strong,{children:"we have large fan-in and mostly the same fan-out"})," (mostly \u2013 because user may have several connections from different devices). We also have Django on the backend and database communication here \u2013 which also makes the use case different as we need to take backend processing timings into account too."]}),"\n",(0,r.jsx)(s.p,{children:"Let's create fake users and fill the rooms with members. First, the function to create fake users programatically:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",metastring:"title='backend/app/utils.py'",children:'from django.contrib.auth.models import User\nfrom django.utils.crypto import get_random_string\nfrom django.contrib.auth.hashers import make_password\n\n\ndef create_users(n):\n users = []\n total = 0\n for _ in range(n):\n username = get_random_string(10)\n email = f"{username}@example.com"\n password = get_random_string(50)\n user = User(username=username, email=email, password=make_password(password, None))\n users.append(user)\n\n if len(users) >= 100:\n total += len(users)\n User.objects.bulk_create(users)\n users = []\n print("Total users created:", total)\n\n # Create remaining users.\n if users:\n total += len(users)\n User.objects.bulk_create(users)\n print("Total users created:", total)\n'})}),"\n",(0,r.jsx)(s.p,{children:"A function to create rooms:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from chat.models import Room\n\n\ndef create_room(name):\n return Room.objects.create(name=name)\n"})}),"\n",(0,r.jsx)(s.p,{children:"Similar helper script may be used to fill the room with users:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:'from chat.models import RoomMember, Room\n\n\ndef fill_room(room_id, limit):\n members = []\n total = 0\n room = Room.objects.get(pk=room_id)\n for user in User.objects.all()[:limit]:\n members.append(RoomMember(room=room, user=user))\n\n if len(members) >= 100:\n total += len(members)\n RoomMember.objects.bulk_create(members, ignore_conflicts=True)\n members = []\n print("Total members created:", total)\n\n # Create remaining members.\n if members:\n total += len(members)\n RoomMember.objects.bulk_create(members, ignore_conflicts=True)\n print("Total members created:", total)\n'})}),"\n",(0,r.jsx)(s.p,{children:"And finally, a function to quickly bootstrap rooms with desired number of members:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"def setup_dev():\n create_users(100_000)\n r1 = create_room('Centrifugo')\n fill_room(r1.pk, 100_000)\n r2 = create_room('Movies')\n fill_room(r2.pk, 10_000)\n r3 = create_room('Programming')\n fill_room(r3.pk, 1_000)\n r4 = create_room('Football')\n fill_room(r4.pk, 100)\n"})}),"\n",(0,r.jsx)(s.p,{children:"To create users connect to Django shell:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"docker compose exec backend python manage.py shell\n"})}),"\n",(0,r.jsx)(s.p,{children:"And run:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from app.utils import setup_dev\nsetup_dev()\n"})}),"\n",(0,r.jsxs)(s.p,{children:["This may take a while, please see how to speed this app in the comment of ",(0,r.jsx)(s.code,{children:"create_users"})," in source code. TLDR - it's possible to relax requirements to password a bit. Which is totally OK for experiment purposes and allows creating 100k users in seconds."]}),"\n",(0,r.jsx)(s.p,{children:"Now, let's compare some latency numbers for these rooms when broadcasting a message. We will measure:"}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"median time of Django handler which processes message creation in every broadcast mode (creation). We have four broadcast modes here: api, outbox, cdc, api_cdc (combined API and CDC)"}),"\n",(0,r.jsx)(s.li,{children:"median time Centrifugo spends on broadcast request (broadcast) - this time is spent by Centrifugo on putting each publication to individual channel from the request, saving publication to each channel's history"}),"\n",(0,r.jsx)(s.li,{children:"end-to-end median latency \u2013 the time between pressing ENTER by a user till receiving real-time message (delivery). This includes passing data over entire stack: Nginx proxy -> Gunicorn/Django -> [api | outbox | cdc | api_cdc ] -> Centrifugo. In practice, in messenger application, only small part of those members will be online at the moment of message broadcast \u2013 in this experiment we will measure the delivery latency while only one client in the room online \u2013 it's OK because having more users connected scales very well in Centrifugo with adding more nodes, so numbers achieved here are totally achievable with more online connections in the room just by adding several more Centrifugo nodes."}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Also note, that in reality there will be some additional overhead due to network latencies missing in this experiment. Our goal here is to show the overhead of technologies used to build the app here. The experiment's goal is to give you the idea of ",(0,r.jsx)(s.strong,{children:"difference"}),", not exact latency values (which may be better or worse depending on the hardware, operating system, etc). All measurements were done on a single local machine \u2013 Apple Macbook M1 Pro \u2013 not very scientific, but fits the goal."]}),"\n",(0,r.jsxs)(s.p,{children:["We first start with Centrifugo that uses ",(0,r.jsx)(s.a,{href:"/docs/server/engines#memory-engine",children:"Memory engine"})," which is the fastest one:"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100"}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 2ms",(0,r.jsx)("br",{}),"delivery 40ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 70ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 140ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 50ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"1k"}),(0,r.jsxs)(s.td,{children:["creation: 60ms",(0,r.jsx)("br",{}),"broadcast: 20ms",(0,r.jsx)("br",{}),"delivery 50ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 75ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 170ms"]}),(0,r.jsxs)(s.td,{children:["creation: 60ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 55ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"10k"}),(0,r.jsxs)(s.td,{children:["creation: 120ms",(0,r.jsx)("br",{}),"broadcast: 60ms",(0,r.jsx)("br",{}),"delivery 115ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 130ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 250ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 150ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 620ms",(0,r.jsx)("br",{}),"broadcast: 520ms",(0,r.jsx)("br",{}),"delivery 600ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 600ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 750ms"]}),(0,r.jsxs)(s.td,{children:["creation: 900ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 750ms"]})]})]})]}),"\n",(0,r.jsx)(s.p,{children:"Things to observe:"}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"end-to-end latency here includes the time Django processes the request, that's why we can't go below 40ms even in rooms with only 100 members."}),"\n",(0,r.jsx)(s.li,{children:"when broadcasting over Centrifugo API - message delivered even faster than Django handler completes its work (since we are publishing synchronously somewhere inside request processing). I.e. this means your frontend can receive real-time message before publish request completes, this is actually true for all other broadcast mode \u2013 just with much smaller probability."}),"\n",(0,r.jsx)(s.li,{children:"using outbox and CDC decreases time of message creation, but latency increases \u2013 since broadcastng is asynchronous, and several more stages involved into the flow. It's generally possible to tune to be faster."}),"\n",(0,r.jsx)(s.li,{children:"for 10k members in group latencies are very acceptable for the messenger app, this is already the scale of quite huge organizations which use Slack messenger, and it's not limit as we will show."}),"\n",(0,r.jsx)(s.li,{children:"using API and CDC together provides better latency than just CDC (so we proved it works as expected!), but probably for large groups you may want to only use CDC to keep publication time reasonably small."}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Now let's use Centrifugo ",(0,r.jsx)(s.a,{href:"/docs/server/engines#redis-engine",children:"Redis engine"}),". In the tutorial we used in-memory engine of Centrifugo. But with Redis engine it's possible to scale Centrifugo nodes and load balance WebSocket connections over them. We left Redis Engine out of the scope in the tutorial \u2013 but you can simply add it by extending ",(0,r.jsx)(s.code,{children:"docker-compose.yml"}),". Here are results we got for it:"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100"}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 6ms",(0,r.jsx)("br",{}),"delivery 50ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 55ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 140ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 50ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"1k"}),(0,r.jsxs)(s.td,{children:["creation: 75ms",(0,r.jsx)("br",{}),"broadcast: 30ms",(0,r.jsx)("br",{}),"delivery 60ms"]}),(0,r.jsxs)(s.td,{children:["creation: 40ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 70ms"]}),(0,r.jsxs)(s.td,{children:["creation: 40ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 180ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 60ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"10k"}),(0,r.jsxs)(s.td,{children:["creation: 240ms",(0,r.jsx)("br",{}),"broadcast: 170ms",(0,r.jsx)("br",{}),"delivery 220ms"]}),(0,r.jsxs)(s.td,{children:["creation: 65ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 250ms"]}),(0,r.jsxs)(s.td,{children:["creation: 65ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 300ms"]}),(0,r.jsxs)(s.td,{children:["creation: 260ms",(0,r.jsx)("br",{}),"broadcast: 180ms",(0,r.jsx)("br",{}),"delivery 260ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 1.5s",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 1.5s"]}),(0,r.jsxs)(s.td,{children:["creation: 140ms",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 2s"]}),(0,r.jsxs)(s.td,{children:["creation: 140ms",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 2s"]}),(0,r.jsxs)(s.td,{children:["creation: 2.8ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 2.6s"]})]})]})]}),"\n",(0,r.jsxs)(s.p,{children:["We see that timings went beyond one second for Redis case for group with 100k members. Since we are sending to 100k ",(0,r.jsx)(s.strong,{children:"individual"})," channels here with saving message history for each, the amount of work is significant. But channel is the unit of scalability in Centrifugo. Let's discuss how we can improve timings in Redis engine case."]}),"\n",(0,r.jsx)(s.p,{children:"First thing to do is adding more Redis instances. Redis operates using a single core of processor, so on modern server machine we can easily start many Redis processes and point Centrifugo to them. Centrifugo will then shard the work between Redis shards. It's also possible to point Centrifugo to a Redis cluster consisting of many nodes."}),"\n",(0,r.jsx)(s.p,{children:"For example, let's start Redis cluster based on 4 nodes and point Centrifugo to it. We then get the following results (skipped 100, 1k and 10k scenarios here as they already fast enough):"}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsx)(s.tbody,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 1s",(0,r.jsx)("br",{}),"broadcast: 900ms",(0,r.jsx)("br",{}),"delivery 950ms"]}),(0,r.jsxs)(s.td,{children:["creation: 220ms",(0,r.jsx)("br",{}),"broadcast: 850ms",(0,r.jsx)("br",{}),"delivery 1s"]}),(0,r.jsxs)(s.td,{children:["creation: 200ms",(0,r.jsx)("br",{}),"broadcast: 850ms",(0,r.jsx)("br",{}),"delivery 1.3s"]}),(0,r.jsxs)(s.td,{children:["creation: 1.6s",(0,r.jsx)("br",{}),"broadcast: 950ms ",(0,r.jsx)("br",{}),"delivery 1.5s"]})]})})]}),"\n",(0,r.jsxs)(s.p,{children:["We can see that latency of broadcasting to 100k channels dropped: ",(0,r.jsx)(s.code,{children:"1.5s -> 900ms"}),". This is because we offloaded some work from a single Redis to several instances."]}),"\n",(0,r.jsx)(s.p,{children:"To reduce the latency of massive broadcast further another concern should be taken into account \u2013 we need to split broadcast to many Centrifugo nodes. Currently all publications inside broadcast request are processed by one Centrifugo node (since all the channels belong to one broadcast request). If we add more Centrifugo nodes and split one broadcast request to several ones to utilize different Centrifugo nodes \u2013 we will parallelize the work of broadcasting the same message to many channels."}),"\n",(0,r.jsxs)(s.p,{children:["You may send parallel requests with splitted batches to Centrifugo HTTP broadcast API (though this requires asynchronous model or using thread pool). Or stick with asynchronous broadcast (outbox, CDC, etc). In this case, make sure to construct batches which belong to different partitions to achieve parallel processing. You can reduce the request up to one channel in batch \u2013 which makes broadcast equal to Centrifugo's ",(0,r.jsx)(s.a,{href:"/docs/server/server_api#publish",children:"publish"})," API."]}),"\n",(0,r.jsx)(s.p,{children:"If you want to keep strict message order then you need to be careful about proper partitioning of processed data. In our app case, we could split channels by user ID. So messages in one broadcast batch belonging to the specific partition may contain stable subset of user IDs (for example, you can apply a hash function to the user ID (or individual channel) and get the reminder of division to some configured number which is much larger that the number of partitions). In that case the order inside one individual user channel will be preserved."}),"\n",(0,r.jsx)(s.p,{children:"After applying these recommendations your requests will be processed in parallel and scale by adding more Centrifugo nodes, more Redis nodes, more partitions. We've made a quick experiment using sth like this in Django code:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from itertools import islice\n\n\ndef chunks(xs, n):\n n = max(1, n)\n iterator = iter(xs)\n return iter(lambda: list(islice(iterator, n)), [])\n\n\nchannel_batches = chunks(channels, 1000)\ncdc_objects = []\ni = 0\nfor batch in channel_batches:\n broadcast_payload = {\n 'channels': batch,\n 'data': {\n 'type': 'message_added',\n 'body': serializer.data\n },\n 'idempotency_key': f'message_{serializer.data[\"id\"]}'\n }\n cdc_objects.append(CDC(method='broadcast', payload=broadcast_payload, partition=i))\n i+=1\n\nCDC.objects.bulk_create(cdc_objects)\n"})}),"\n",(0,r.jsx)(s.admonition,{type:"caution",children:(0,r.jsx)(s.p,{children:"Note, this code does not properly partition data, so may result into incorrect ordering - was used just to prove the idea!"})}),"\n",(0,r.jsx)(s.p,{children:"With this batch approach and running Centrifugo with 8 isolated Redis instances and Centrifugo's client-side Redis sharding feature we were able to quickly achieve 400ms median delivery latency on Digital Ocean 16 CPU-core droplet. So sending a message to a group with 100k members feels almost instant:"}),"\n",(0,r.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/grand-chat-100k.mp4"}),"\n",(0,r.jsx)(s.p,{children:"If we take Slack as an example, this already feels nice to cover messaging needs of some largest organizations in the world. It will also work for Amazon scale, who has around 1.5 million people now \u2013 just need more resources for better end-to-end latency or simply trade-off the latency in large messenger groups for reduced resources."}),"\n",(0,r.jsx)(s.p,{children:"To conclude here, scaling messenger apps requires careful thinking. The complexity in the case goes from the fact we are using personal channels for message delivery - thus we have a massive fan-in and need to use broadcast API of Centrifugo."}),"\n",(0,r.jsxs)(s.p,{children:["If we had isolated chat rooms (for example, like real-time comments for each video on Youtube web site) \u2013 then it could be much easier to implement and to scale. Because we could just subscribe to the specific room channel instead of user individual channel and publish only to one channel on every message sent (using Centrifugo ",(0,r.jsx)(s.a,{href:"/docs/server/server_api#publish",children:"simple publish API"}),"). It's a very small fan-in and the scalability with many concurrent users may be simply achieved by adding more Centrifugo nodes. Also, if we had only one-to-one chat rooms in the app, without super-groups with 100k members \u2013 again, it scales pretty easily. If you don't need message recovery \u2013 than disabling it will provide better performance too. Our experiments with 100k members and a single ",(0,r.jsx)(s.a,{href:"/docs/server/engines#nats-broker",children:"Nats server as broker"})," showed 300ms delivery latency."]}),"\n",(0,r.jsx)(s.p,{children:"But when we design an app where we want to have a screen with all user's rooms, where some rooms have massive number of members, and need to consume updates from all of them \u2013 things are becoming harder as we've just shown above. That's an important thing to keep in mind - application specifics may affect Centrifugo channel configuration and performance a lot."})]})}function h(e={}){const{wrapper:s}={...(0,t.a)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},47692:(e,s,n)=>{n.d(s,{Z:()=>r});const r=n.p+"assets/images/spin_chat_cover-a3bc9f6c512a0b8394c62f8edec25c8c.jpg"},11151:(e,s,n)=>{n.d(s,{Z:()=>a,a:()=>i});var r=n(67294);const t={},o=r.createContext(t);function i(e){const s=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function a(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),r.createElement(o.Provider,{value:s},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7973],{94567:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>d});var r=n(85893),t=n(11151);const o={id:"scale",sidebar_label:"Scale to 100k room members",title:"Scale to 100k cats in room"},i=void 0,a={id:"tutorial/scale",title:"Scale to 100k cats in room",description:"Congratulations \u2013 we've built an awesome app and we are done with the development within this tutorial! \ud83c\udf89",source:"@site/docs/tutorial/scale.md",sourceDirName:"tutorial",slug:"/tutorial/scale",permalink:"/docs/tutorial/scale",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/scale.md",tags:[],version:"current",frontMatter:{id:"scale",sidebar_label:"Scale to 100k room members",title:"Scale to 100k cats in room"},sidebar:"Tutorial",previous:{title:"Broadcast: outbox and CDC",permalink:"/docs/tutorial/outbox_cdc"},next:{title:"Wrapping up \u2013 things learnt",permalink:"/docs/tutorial/outro"}},c={},d=[];function l(e){const s={a:"a",admonition:"admonition",code:"code",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.p,{children:"Congratulations \u2013 we've built an awesome app and we are done with the development within this tutorial! \ud83c\udf89"}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.img,{src:n(23842).Z+"",width:"2740",height:"1510"})}),"\n",(0,r.jsx)(s.p,{children:"But before wrapping up, let's experiment a little. Here we will try to look at some latency numbers for the room with 100, 1k, 10k, 100k members in different scenarios. Not many apps will reach 100k members in one group scale, but we want to show that Centrifugo gives you a way to grow this big keeping reasonable latency times, and also gives answers how to reduce latency and increase a system throughput further."}),"\n",(0,r.jsx)(s.admonition,{type:"info",children:(0,r.jsxs)(s.p,{children:["This chapter is still to be improved. We've included some numbers we were able to get while experimenting with the app \u2013 but left configuring Redis Engine out of scope for now. In experiments we used Centrifugo running outside of Docker. See how we did it in ",(0,r.jsx)(s.a,{href:"/docs/tutorial/tips_and_tricks#point-to-centrifugo-running-on-host-outside-docker",children:"Tips and Tricks"}),"."]})}),"\n",(0,r.jsxs)(s.p,{children:["In our blog post ",(0,r.jsx)(s.a,{href:"/blog/2020/02/10/million-connections-with-centrifugo",children:"Million connections with Centrifugo"})," we've shown that on a limited hardware resources, comparable to one modern server machine, delivering 500k messages per second with delivery latency no more than 200ms in 99 percentile is possible with Centrifugo."]}),"\n",(0,r.jsxs)(s.p,{children:["But this case is different, in this app we want to have large group chats with many members. The difference here is that publishing involves sending a message to each individual channel \u2013 so instead of small fan-in and large fan-out ",(0,r.jsx)(s.strong,{children:"we have large fan-in and mostly the same fan-out"})," (mostly \u2013 because user may have several connections from different devices). We also have Django on the backend and database communication here \u2013 which also makes the use case different as we need to take backend processing timings into account too."]}),"\n",(0,r.jsx)(s.p,{children:"Let's create fake users and fill the rooms with members. First, the function to create fake users programatically:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",metastring:"title='backend/app/utils.py'",children:'from django.contrib.auth.models import User\nfrom django.utils.crypto import get_random_string\nfrom django.contrib.auth.hashers import make_password\n\n\ndef create_users(n):\n users = []\n total = 0\n for _ in range(n):\n username = get_random_string(10)\n email = f"{username}@example.com"\n password = get_random_string(50)\n user = User(username=username, email=email, password=make_password(password, None))\n users.append(user)\n\n if len(users) >= 100:\n total += len(users)\n User.objects.bulk_create(users)\n users = []\n print("Total users created:", total)\n\n # Create remaining users.\n if users:\n total += len(users)\n User.objects.bulk_create(users)\n print("Total users created:", total)\n'})}),"\n",(0,r.jsx)(s.p,{children:"A function to create rooms:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from chat.models import Room\n\n\ndef create_room(name):\n return Room.objects.create(name=name)\n"})}),"\n",(0,r.jsx)(s.p,{children:"Similar helper script may be used to fill the room with users:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:'from chat.models import RoomMember, Room\n\n\ndef fill_room(room_id, limit):\n members = []\n total = 0\n room = Room.objects.get(pk=room_id)\n for user in User.objects.all()[:limit]:\n members.append(RoomMember(room=room, user=user))\n\n if len(members) >= 100:\n total += len(members)\n RoomMember.objects.bulk_create(members, ignore_conflicts=True)\n members = []\n print("Total members created:", total)\n\n # Create remaining members.\n if members:\n total += len(members)\n RoomMember.objects.bulk_create(members, ignore_conflicts=True)\n print("Total members created:", total)\n'})}),"\n",(0,r.jsx)(s.p,{children:"And finally, a function to quickly bootstrap rooms with desired number of members:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"def setup_dev():\n create_users(100_000)\n r1 = create_room('Centrifugo')\n fill_room(r1.pk, 100_000)\n r2 = create_room('Movies')\n fill_room(r2.pk, 10_000)\n r3 = create_room('Programming')\n fill_room(r3.pk, 1_000)\n r4 = create_room('Football')\n fill_room(r4.pk, 100)\n"})}),"\n",(0,r.jsx)(s.p,{children:"To create users connect to Django shell:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"docker compose exec backend python manage.py shell\n"})}),"\n",(0,r.jsx)(s.p,{children:"And run:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from app.utils import setup_dev\nsetup_dev()\n"})}),"\n",(0,r.jsxs)(s.p,{children:["This may take a while, please see how to speed this app in the comment of ",(0,r.jsx)(s.code,{children:"create_users"})," in source code. TLDR - it's possible to relax requirements to password a bit. Which is totally OK for experiment purposes and allows creating 100k users in seconds."]}),"\n",(0,r.jsx)(s.p,{children:"Now, let's compare some latency numbers for these rooms when broadcasting a message. We will measure:"}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"median time of Django handler which processes message creation in every broadcast mode (creation). We have four broadcast modes here: api, outbox, cdc, api_cdc (combined API and CDC)"}),"\n",(0,r.jsx)(s.li,{children:"median time Centrifugo spends on broadcast request (broadcast) - this time is spent by Centrifugo on putting each publication to individual channel from the request, saving publication to each channel's history"}),"\n",(0,r.jsx)(s.li,{children:"end-to-end median latency \u2013 the time between pressing ENTER by a user till receiving real-time message (delivery). This includes passing data over entire stack: Nginx proxy -> Gunicorn/Django -> [api | outbox | cdc | api_cdc ] -> Centrifugo. In practice, in messenger application, only small part of those members will be online at the moment of message broadcast \u2013 in this experiment we will measure the delivery latency while only one client in the room online \u2013 it's OK because having more users connected scales very well in Centrifugo with adding more nodes, so numbers achieved here are totally achievable with more online connections in the room just by adding several more Centrifugo nodes."}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Also note, that in reality there will be some additional overhead due to network latencies missing in this experiment. Our goal here is to show the overhead of technologies used to build the app here. The experiment's goal is to give you the idea of ",(0,r.jsx)(s.strong,{children:"difference"}),", not exact latency values (which may be better or worse depending on the hardware, operating system, etc). All measurements were done on a single local machine \u2013 Apple Macbook M1 Pro \u2013 not very scientific, but fits the goal."]}),"\n",(0,r.jsxs)(s.p,{children:["We first start with Centrifugo that uses ",(0,r.jsx)(s.a,{href:"/docs/server/engines#memory-engine",children:"Memory engine"})," which is the fastest one:"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100"}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 2ms",(0,r.jsx)("br",{}),"delivery 40ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 70ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 140ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 1ms",(0,r.jsx)("br",{}),"delivery 50ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"1k"}),(0,r.jsxs)(s.td,{children:["creation: 60ms",(0,r.jsx)("br",{}),"broadcast: 20ms",(0,r.jsx)("br",{}),"delivery 50ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 75ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 170ms"]}),(0,r.jsxs)(s.td,{children:["creation: 60ms",(0,r.jsx)("br",{}),"broadcast: 18ms",(0,r.jsx)("br",{}),"delivery 55ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"10k"}),(0,r.jsxs)(s.td,{children:["creation: 120ms",(0,r.jsx)("br",{}),"broadcast: 60ms",(0,r.jsx)("br",{}),"delivery 115ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 130ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 250ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 55ms",(0,r.jsx)("br",{}),"delivery 150ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 620ms",(0,r.jsx)("br",{}),"broadcast: 520ms",(0,r.jsx)("br",{}),"delivery 600ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 600ms"]}),(0,r.jsxs)(s.td,{children:["creation: 170ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 750ms"]}),(0,r.jsxs)(s.td,{children:["creation: 900ms",(0,r.jsx)("br",{}),"broadcast: 500ms",(0,r.jsx)("br",{}),"delivery 750ms"]})]})]})]}),"\n",(0,r.jsx)(s.p,{children:"Things to observe:"}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"end-to-end latency here includes the time Django processes the request, that's why we can't go below 40ms even in rooms with only 100 members."}),"\n",(0,r.jsx)(s.li,{children:"when broadcasting over Centrifugo API - message delivered even faster than Django handler completes its work (since we are publishing synchronously somewhere inside request processing). I.e. this means your frontend can receive real-time message before publish request completes, this is actually true for all other broadcast mode \u2013 just with much smaller probability."}),"\n",(0,r.jsx)(s.li,{children:"using outbox and CDC decreases time of message creation, but latency increases \u2013 since broadcastng is asynchronous, and several more stages involved into the flow. It's generally possible to tune to be faster."}),"\n",(0,r.jsx)(s.li,{children:"for 10k members in group latencies are very acceptable for the messenger app, this is already the scale of quite huge organizations which use Slack messenger, and it's not limit as we will show."}),"\n",(0,r.jsx)(s.li,{children:"using API and CDC together provides better latency than just CDC (so we proved it works as expected!), but probably for large groups you may want to only use CDC to keep publication time reasonably small."}),"\n"]}),"\n",(0,r.jsxs)(s.p,{children:["Now let's use Centrifugo ",(0,r.jsx)(s.a,{href:"/docs/server/engines#redis-engine",children:"Redis engine"}),". In the tutorial we used in-memory engine of Centrifugo. But with Redis engine it's possible to scale Centrifugo nodes and load balance WebSocket connections over them. We left Redis Engine out of the scope in the tutorial \u2013 but you can simply add it by extending ",(0,r.jsx)(s.code,{children:"docker-compose.yml"}),". Here are results we got for it:"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100"}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 6ms",(0,r.jsx)("br",{}),"delivery 50ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 55ms"]}),(0,r.jsxs)(s.td,{children:["creation: 35ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 140ms"]}),(0,r.jsxs)(s.td,{children:["creation: 55ms",(0,r.jsx)("br",{}),"broadcast: 5ms",(0,r.jsx)("br",{}),"delivery 50ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"1k"}),(0,r.jsxs)(s.td,{children:["creation: 75ms",(0,r.jsx)("br",{}),"broadcast: 30ms",(0,r.jsx)("br",{}),"delivery 60ms"]}),(0,r.jsxs)(s.td,{children:["creation: 40ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 70ms"]}),(0,r.jsxs)(s.td,{children:["creation: 40ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 180ms"]}),(0,r.jsxs)(s.td,{children:["creation: 50ms",(0,r.jsx)("br",{}),"broadcast: 25ms",(0,r.jsx)("br",{}),"delivery 60ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"10k"}),(0,r.jsxs)(s.td,{children:["creation: 240ms",(0,r.jsx)("br",{}),"broadcast: 170ms",(0,r.jsx)("br",{}),"delivery 220ms"]}),(0,r.jsxs)(s.td,{children:["creation: 65ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 250ms"]}),(0,r.jsxs)(s.td,{children:["creation: 65ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 300ms"]}),(0,r.jsxs)(s.td,{children:["creation: 260ms",(0,r.jsx)("br",{}),"broadcast: 180ms",(0,r.jsx)("br",{}),"delivery 260ms"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 1.5s",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 1.5s"]}),(0,r.jsxs)(s.td,{children:["creation: 140ms",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 2s"]}),(0,r.jsxs)(s.td,{children:["creation: 140ms",(0,r.jsx)("br",{}),"broadcast: 1.4s",(0,r.jsx)("br",{}),"delivery 2s"]}),(0,r.jsxs)(s.td,{children:["creation: 2.8ms",(0,r.jsx)("br",{}),"broadcast: 160ms",(0,r.jsx)("br",{}),"delivery 2.6s"]})]})]})]}),"\n",(0,r.jsxs)(s.p,{children:["We see that timings went beyond one second for Redis case for group with 100k members. Since we are sending to 100k ",(0,r.jsx)(s.strong,{children:"individual"})," channels here with saving message history for each, the amount of work is significant. But channel is the unit of scalability in Centrifugo. Let's discuss how we can improve timings in Redis engine case."]}),"\n",(0,r.jsx)(s.p,{children:"First thing to do is adding more Redis instances. Redis operates using a single core of processor, so on modern server machine we can easily start many Redis processes and point Centrifugo to them. Centrifugo will then shard the work between Redis shards. It's also possible to point Centrifugo to a Redis cluster consisting of many nodes."}),"\n",(0,r.jsx)(s.p,{children:"For example, let's start Redis cluster based on 4 nodes and point Centrifugo to it. We then get the following results (skipped 100, 1k and 10k scenarios here as they already fast enough):"}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{}),(0,r.jsx)(s.th,{children:"api"}),(0,r.jsx)(s.th,{children:"outbox"}),(0,r.jsx)(s.th,{children:"cdc"}),(0,r.jsx)(s.th,{children:"api_cdc"})]})}),(0,r.jsx)(s.tbody,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"100k"}),(0,r.jsxs)(s.td,{children:["creation: 1s",(0,r.jsx)("br",{}),"broadcast: 900ms",(0,r.jsx)("br",{}),"delivery 950ms"]}),(0,r.jsxs)(s.td,{children:["creation: 220ms",(0,r.jsx)("br",{}),"broadcast: 850ms",(0,r.jsx)("br",{}),"delivery 1s"]}),(0,r.jsxs)(s.td,{children:["creation: 200ms",(0,r.jsx)("br",{}),"broadcast: 850ms",(0,r.jsx)("br",{}),"delivery 1.3s"]}),(0,r.jsxs)(s.td,{children:["creation: 1.6s",(0,r.jsx)("br",{}),"broadcast: 950ms ",(0,r.jsx)("br",{}),"delivery 1.5s"]})]})})]}),"\n",(0,r.jsxs)(s.p,{children:["We can see that latency of broadcasting to 100k channels dropped: ",(0,r.jsx)(s.code,{children:"1.5s -> 900ms"}),". This is because we offloaded some work from a single Redis to several instances."]}),"\n",(0,r.jsx)(s.p,{children:"To reduce the latency of massive broadcast further another concern should be taken into account \u2013 we need to split broadcast to many Centrifugo nodes. Currently all publications inside broadcast request are processed by one Centrifugo node (since all the channels belong to one broadcast request). If we add more Centrifugo nodes and split one broadcast request to several ones to utilize different Centrifugo nodes \u2013 we will parallelize the work of broadcasting the same message to many channels."}),"\n",(0,r.jsxs)(s.p,{children:["You may send parallel requests with splitted batches to Centrifugo HTTP broadcast API (though this requires asynchronous model or using thread pool). Or stick with asynchronous broadcast (outbox, CDC, etc). In this case, make sure to construct batches which belong to different partitions to achieve parallel processing. You can reduce the request up to one channel in batch \u2013 which makes broadcast equal to Centrifugo's ",(0,r.jsx)(s.a,{href:"/docs/server/server_api#publish",children:"publish"})," API."]}),"\n",(0,r.jsx)(s.p,{children:"If you want to keep strict message order then you need to be careful about proper partitioning of processed data. In our app case, we could split channels by user ID. So messages in one broadcast batch belonging to the specific partition may contain stable subset of user IDs (for example, you can apply a hash function to the user ID (or individual channel) and get the reminder of division to some configured number which is much larger that the number of partitions). In that case the order inside one individual user channel will be preserved."}),"\n",(0,r.jsx)(s.p,{children:"After applying these recommendations your requests will be processed in parallel and scale by adding more Centrifugo nodes, more Redis nodes, more partitions. We've made a quick experiment using sth like this in Django code:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-python",children:"from itertools import islice\n\n\ndef chunks(xs, n):\n n = max(1, n)\n iterator = iter(xs)\n return iter(lambda: list(islice(iterator, n)), [])\n\n\nchannel_batches = chunks(channels, 1000)\ncdc_objects = []\ni = 0\nfor batch in channel_batches:\n broadcast_payload = {\n 'channels': batch,\n 'data': {\n 'type': 'message_added',\n 'body': serializer.data\n },\n 'idempotency_key': f'message_{serializer.data[\"id\"]}'\n }\n cdc_objects.append(CDC(method='broadcast', payload=broadcast_payload, partition=i))\n i+=1\n\nCDC.objects.bulk_create(cdc_objects)\n"})}),"\n",(0,r.jsx)(s.admonition,{type:"caution",children:(0,r.jsx)(s.p,{children:"Note, this code does not properly partition data, so may result into incorrect ordering - was used just to prove the idea!"})}),"\n",(0,r.jsx)(s.p,{children:"With this batch approach and running Centrifugo with 8 isolated Redis instances and Centrifugo's client-side Redis sharding feature we were able to quickly achieve 400ms median delivery latency on Digital Ocean 16 CPU-core droplet. So sending a message to a group with 100k members feels almost instant:"}),"\n",(0,r.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/grand-chat-100k.mp4"}),"\n",(0,r.jsx)(s.p,{children:"If we take Slack as an example, this already feels nice to cover messaging needs of some largest organizations in the world. It will also work for Amazon scale, who has around 1.5 million people now \u2013 just need more resources for better end-to-end latency or simply trade-off the latency in large messenger groups for reduced resources."}),"\n",(0,r.jsx)(s.p,{children:"To conclude here, scaling messenger apps requires careful thinking. The complexity in the case goes from the fact we are using personal channels for message delivery - thus we have a massive fan-in and need to use broadcast API of Centrifugo."}),"\n",(0,r.jsxs)(s.p,{children:["If we had isolated chat rooms (for example, like real-time comments for each video on Youtube web site) \u2013 then it could be much easier to implement and to scale. Because we could just subscribe to the specific room channel instead of user individual channel and publish only to one channel on every message sent (using Centrifugo ",(0,r.jsx)(s.a,{href:"/docs/server/server_api#publish",children:"simple publish API"}),"). It's a very small fan-in and the scalability with many concurrent users may be simply achieved by adding more Centrifugo nodes. Also, if we had only one-to-one chat rooms in the app, without super-groups with 100k members \u2013 again, it scales pretty easily. If you don't need message recovery \u2013 than disabling it will provide better performance too. Our experiments with 100k members and a single ",(0,r.jsx)(s.a,{href:"/docs/server/engines#nats-broker",children:"Nats server as broker"})," showed 300ms delivery latency."]}),"\n",(0,r.jsx)(s.p,{children:"But when we design an app where we want to have a screen with all user's rooms, where some rooms have massive number of members, and need to consume updates from all of them \u2013 things are becoming harder as we've just shown above. That's an important thing to keep in mind - application specifics may affect Centrifugo channel configuration and performance a lot."})]})}function h(e={}){const{wrapper:s}={...(0,t.a)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},23842:(e,s,n)=>{n.d(s,{Z:()=>r});const r=n.p+"assets/images/spin_chat_cover-a3bc9f6c512a0b8394c62f8edec25c8c.jpg"},11151:(e,s,n)=>{n.d(s,{Z:()=>a,a:()=>i});var r=n(67294);const t={},o=r.createContext(t);function i(e){const s=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function a(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),r.createElement(o.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/211f1d7a.116dc560.js b/assets/js/211f1d7a.116dc560.js deleted file mode 100644 index 094abc895..000000000 --- a/assets/js/211f1d7a.116dc560.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2540],{30433:(e,n,t)=>{t.d(n,{Z:()=>r});t(67294);var i=t(36905);const s={tabItem:"tabItem_Ymn6"};var o=t(85893);function r(e){let{children:n,hidden:t,className:r}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,i.Z)(s.tabItem,r),hidden:t,children:n})}},22808:(e,n,t)=>{t.d(n,{Z:()=>w});var i=t(67294),s=t(36905),o=t(63735),r=t(16550),a=t(20613),c=t(34423),l=t(20636),d=t(99200);function h(e){return i.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,i.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:i,default:s}}=e;return{value:n,label:t,attributes:i,default:s}}))}(t);return function(e){const n=(0,l.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const s=(0,r.k6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,c._X)(o),(0,i.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(s.location.search);n.set(o,e),s.replace({...s.location,search:n.toString()})}),[o,s])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,o=u(e),[r,c]=(0,i.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const i=t.find((e=>e.default))??t[0];if(!i)throw new Error("Unexpected error: 0 tabValues");return i.value}({defaultValue:n,tabValues:o}))),[l,h]=x({queryString:t,groupId:s}),[j,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,o]=(0,d.Nk)(t);return[s,(0,i.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:s}),m=(()=>{const e=l??j;return p({value:e,tabValues:o})?e:null})();(0,a.Z)((()=>{m&&c(m)}),[m]);return{selectedValue:r,selectValue:(0,i.useCallback)((e=>{if(!p({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);c(e),h(e),f(e)}),[h,f,o]),tabValues:o}}var f=t(5730);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(85893);function g(e){let{className:n,block:t,selectedValue:i,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),d=e=>{const n=e.currentTarget,t=c.indexOf(n),s=a[t].value;s!==i&&(l(n),r(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=c.indexOf(e.currentTarget)+1;n=c[t]??c[0];break}case"ArrowLeft":{const t=c.indexOf(e.currentTarget)-1;n=c[t]??c[c.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:o}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...o,className:(0,s.Z)("tabs__item",m.tabItem,o?.className,{"tabs__item--active":i===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:s}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===s));return e?(0,i.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function y(e){const n=j(e);return(0,b.jsxs)("div",{className:(0,s.Z)("tabs-container",m.tabList),children:[(0,b.jsx)(g,{...n,...e}),(0,b.jsx)(v,{...n,...e})]})}function w(e){const n=(0,f.Z)();return(0,b.jsx)(y,{...e,children:h(e.children)},String(n))}},70733:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var i=t(85893),s=t(11151),o=t(22808),r=t(30433);const a={id:"authentication",title:"Client authentication"},c=void 0,l={id:"server/authentication",title:"Client authentication",description:"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.",source:"@site/versioned_docs/version-3/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/3/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/server/authentication.md",tags:[],version:"3",frontMatter:{id:"authentication",title:"Client authentication"},sidebar:"Guides",previous:{title:"Server API",permalink:"/docs/3/server/server_api"},next:{title:"Channels",permalink:"/docs/3/server/channels"}},d={},h=[{value:"Claims",id:"claims",level:2},{value:"sub",id:"sub",level:3},{value:"exp",id:"exp",level:3},{value:"iat",id:"iat",level:3},{value:"jti",id:"jti",level:3},{value:"aud",id:"aud",level:3},{value:"iss",id:"iss",level:3},{value:"info",id:"info",level:3},{value:"b64info",id:"b64info",level:3},{value:"channels",id:"channels",level:3},{value:"subs",id:"subs",level:3},{value:"Subscribe options:",id:"subscribe-options",level:4},{value:"Override object",id:"override-object",level:4},{value:"meta",id:"meta",level:3},{value:"expire_at",id:"expire_at",level:3},{value:"Connection expiration",id:"connection-expiration",level:2},{value:"Examples",id:"examples",level:2},{value:"Simplest token",id:"simplest-token",level:3},{value:"Token with expiration",id:"token-with-expiration",level:3},{value:"Token with additional connection info",id:"token-with-additional-connection-info",level:3},{value:"Investigating problems with JWT",id:"investigating-problems-with-jwt",level:3},{value:"JSON Web Key support",id:"json-web-key-support",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["To authenticate incoming connection (client) Centrifugo can use ",(0,i.jsx)(n.a,{href:"https://jwt.io/introduction",children:"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."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["If you prefer to avoid using JWT then look at ",(0,i.jsx)(n.a,{href:"/docs/3/server/proxy",children:"the proxy feature"}),". It allows proxying connection requests from Centrifugo to your application backend for authentication details."]})}),"\n",(0,i.jsxs)(n.p,{children:["Upon connecting to Centrifugo client should provide a connection JWT with several predefined credential claims. If you've never heard about JWT before - refer to ",(0,i.jsx)(n.a,{href:"https://jwt.io/",children:"jwt.io"})," page."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{src:t(32691).Z+"",width:"2600",height:"906"})}),"\n",(0,i.jsx)(n.p,{children:"At the moment Centrifugo supports HMAC, RSA and ECDSA JWT algorithms - i.e. HS256, HS384, HS512, RSA256, RSA384, RSA512, EC256, EC384, EC512."}),"\n",(0,i.jsxs)(n.p,{children:["We will use Javascript Centrifugo client here for example snippets for client-side and ",(0,i.jsx)(n.a,{href:"https://github.com/jpadilla/pyjwt",children:"PyJWT"})," Python library to generate a connection token on the backend side."]}),"\n",(0,i.jsxs)(n.p,{children:["To add HMAC secret key to Centrifugo add ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," to configuration file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_hmac_secret_key": "<YOUR-SECRET-STRING-HERE>"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To add RSA public key (must be PEM encoded string) add ",(0,i.jsx)(n.code,{children:"token_rsa_public_key"})," option, ex:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_rsa_public_key": "-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZ..."\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To add ECDSA public key (must be PEM encoded string) add ",(0,i.jsx)(n.code,{children:"token_ecdsa_public_key"})," option, ex:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_ecdsa_public_key": "-----BEGIN PUBLIC KEY-----\\nxyz23adf..."\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"claims",children:"Claims"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo uses the following claims in a JWT: ",(0,i.jsx)(n.code,{children:"sub"}),", ",(0,i.jsx)(n.code,{children:"exp"}),", ",(0,i.jsx)(n.code,{children:"iat"}),", ",(0,i.jsx)(n.code,{children:"jti"}),", ",(0,i.jsx)(n.code,{children:"info"}),", ",(0,i.jsx)(n.code,{children:"b64info"}),", ",(0,i.jsx)(n.code,{children:"channels"}),", ",(0,i.jsx)(n.code,{children:"subs"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,i.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,i.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["If a user is not currently authenticated in an application, but you want to let him connect to Centrifugo anyway \u2013 you can use an empty string as a user ID in ",(0,i.jsx)(n.code,{children:"sub"})," claim. This is called anonymous access. In this case, the ",(0,i.jsx)(n.code,{children:"anonymous"})," option must be enabled in Centrifugo configuration for channels that the client will subscribe to."]}),"\n",(0,i.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,i.jsx)(n.p,{children:"This is a UNIX timestamp seconds when the token will expire. This is a standard JWT claim - all JWT libraries for different languages provide an API to set it."}),"\n",(0,i.jsxs)(n.p,{children:["If ",(0,i.jsx)(n.code,{children:"exp"})," claim is not provided then Centrifugo won't expire connection. When provided special algorithm will find connections with ",(0,i.jsx)(n.code,{children:"exp"})," in the past and activate the connection refresh mechanism. Refresh mechanism allows connection to survive and be prolonged. In case of refresh failure, the client connection will be eventually closed by Centrifugo and won't be accepted until new valid and actual credentials are provided in the connection token."]}),"\n",(0,i.jsx)(n.p,{children:"You can use the connection expiration mechanism in cases when you don't want users of your app to be subscribed on channels after being banned/deactivated in the application. Or to protect your users from token leakage (providing a reasonably short time of expiration)."}),"\n",(0,i.jsxs)(n.p,{children:["Choose ",(0,i.jsx)(n.code,{children:"exp"})," value wisely, you don't need small values because the refresh mechanism will hit your application often with refresh requests. But setting this value too large can lead to slow user connection deactivation. This is a trade-off."]}),"\n",(0,i.jsxs)(n.p,{children:["Read more about connection expiration ",(0,i.jsx)(n.a,{href:"#connection-expiration",children:"below"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,i.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6",children:"definition in RFC"}),". This claim is optional but can be useful together with ",(0,i.jsx)(n.a,{href:"/docs/3/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,i.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7",children:"definition in RFC"}),". This claim is optional but can be useful together with ",(0,i.jsx)(n.a,{href:"/docs/3/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,i.jsx)(n.p,{children:"Handled since Centrifugo v3.2.0"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim)."]}),"\n",(0,i.jsxs)(n.p,{children:["But you can force this check by setting ",(0,i.jsx)(n.code,{children:"token_audience"})," string option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_audience": "centrifugo"\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_audience"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/3/server/private_channels",children:"private channels"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,i.jsx)(n.p,{children:"Handled since Centrifugo v3.2.0"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim)."]}),"\n",(0,i.jsxs)(n.p,{children:["But you can force this check by setting ",(0,i.jsx)(n.code,{children:"token_issuer"})," string option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_issuer": "my_app"\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_issuer"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/3/server/private_channels",children:"private channels"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,i.jsx)(n.p,{children:"This claim is optional - this is additional information about client connection that can be provided for Centrifugo. This information will be included in presence information, join/leave events, and channel publication if it was published from a client-side."}),"\n",(0,i.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,i.jsx)(n.p,{children:"If you are using binary Protobuf protocol you may want info to be custom bytes. Use this field in this case."}),"\n",(0,i.jsxs)(n.p,{children:["This field contains a ",(0,i.jsx)(n.code,{children:"base64"})," representation of your bytes. After receiving Centrifugo will decode base64 back to bytes and will embed the result into various places described above."]}),"\n",(0,i.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,i.jsxs)(n.p,{children:["An optional array of strings with server-side channels to subscribe a client to. See more details about ",(0,i.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["By providing a list of channels in JWT with ",(0,i.jsx)(n.code,{children:"channels"})," claim you are not making them automatically unaccessible by other users. Other users can still call a client-side ",(0,i.jsx)(n.code,{children:".subscribe()"})," method and subscribe to these channels if channel permissions allow doing this. If you need to protect channels from being subscribed by other connections then you can use private channels inside this ",(0,i.jsx)(n.code,{children:"channels"})," array (i.e. starting with ",(0,i.jsx)(n.code,{children:"$"}),") or turn on ",(0,i.jsx)(n.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option for channels namespaces."]})}),"\n",(0,i.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,i.jsxs)(n.p,{children:["An optional map of channels with options. This is like a ",(0,i.jsx)(n.code,{children:"channels"})," claim but allows more control over server-side subscription since every channel can be annotated with info, data, and so on using options."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["This claim is called ",(0,i.jsx)(n.code,{children:"subs"})," as a shortcut from subscriptions. The claim ",(0,i.jsx)(n.code,{children:"sub"})," described above is a standart JWT claim to provide a user ID (it's a shortcut from subject). While claims have similar names they have different purpose in a connection JWT."]})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["By providing a map of channels in JWT with ",(0,i.jsx)(n.code,{children:"subs"})," claim you are not making channels automatically unaccessible by other users. Other users can still call a client-side ",(0,i.jsx)(n.code,{children:".subscribe()"})," method and subscribe to these channels if channel permissions allow doing this. If you need to protect channels from being subscribed by other connections then you can use private channels inside this ",(0,i.jsx)(n.code,{children:"subs"})," map (i.e. starting with ",(0,i.jsx)(n.code,{children:"$"}),") or turn on ",(0,i.jsx)(n.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option for channels namespaces."]})}),"\n",(0,i.jsx)(n.p,{children:"Example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "subs": {\n "channel1": {\n "data": {"welcome": "welcome to channel1"}\n },\n "channel2": {\n "data": {"welcome": "welcome to channel2"}\n }\n }\n}\n'})}),"\n",(0,i.jsx)(n.h4,{id:"subscribe-options",children:"Subscribe options:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Optional"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"info"}),(0,i.jsx)(n.td,{children:"JSON object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom channel info"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"b64info"}),(0,i.jsx)(n.td,{children:"string"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom channel info in Base64 - to pass binary channel info"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"data"}),(0,i.jsx)(n.td,{children:"JSON object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom JSON data to return in subscription context inside Connect reply"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"b64data"}),(0,i.jsx)(n.td,{children:"string"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsxs)(n.td,{children:["Same as ",(0,i.jsx)(n.code,{children:"data"})," but in Base64 to send binary data"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"override"}),(0,i.jsx)(n.td,{children:"Override object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,i.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Optional"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"presence"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override presence"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"join_leave"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override join_leave"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"position"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override position"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"recover"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"meta",children:"meta"}),"\n",(0,i.jsxs)(n.p,{children:["Meta is an additional JSON object (ex. ",(0,i.jsx)(n.code,{children:'{"key": "value"}'}),") that will be attached to a connection. Unlike ",(0,i.jsx)(n.code,{children:"info"})," it's never exposed to clients and only accessible on a backend side. It will be included in proxy calls from Centrifugo to the application backend. Also, there is a ",(0,i.jsx)(n.code,{children:"get_user_connections"})," API method in Centrifugo PRO that returns this data in the user connection object."]}),"\n",(0,i.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo looks on ",(0,i.jsx)(n.code,{children:"exp"})," claim to configure connection expiration. In most cases this is fine, but there could be situations where you wish to decouple token expiration check with connection expiration time. As soon as the ",(0,i.jsx)(n.code,{children:"expire_at"})," claim is provided (set) in JWT Centrifugo relies on it for setting connection expiration time (JWT expiration still checked over ",(0,i.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the connection should expire."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Set it to the future time for expiring connection at some point"}),"\n",(0,i.jsxs)(n.li,{children:["Set it to ",(0,i.jsx)(n.code,{children:"0"})," to disable connection expiration (but still check token ",(0,i.jsx)(n.code,{children:"exp"})," claim)."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,i.jsxs)(n.p,{children:["As said above ",(0,i.jsx)(n.code,{children:"exp"})," claim in a connection token allows expiring client connection at some point in time. Let's look in detail at what happens when Centrifugo detects that the connection is going to expire."]}),"\n",(0,i.jsx)(n.p,{children:"First, you should do is enable client expiration mechanism in Centrifugo providing a connection JWT with expiration:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\ntoken = jwt.encode({"sub": "42", "exp": int(time.time()) + 10*60}, "secret").decode()\n\nprint(token)\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Let's suppose that you set ",(0,i.jsx)(n.code,{children:"exp"})," field to timestamp that will expire in 10 minutes and the client connected to Centrifugo with this token. During 10 minutes the connection will be kept by Centrifugo. When this time passed Centrifugo gives the connection some time (configured, 25 seconds by default) to refresh its credentials and provide a new valid token with new ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When a client first connects to Centrifugo it receives the ",(0,i.jsx)(n.code,{children:"ttl"})," value in connect reply. That ",(0,i.jsx)(n.code,{children:"ttl"})," value contains the number of seconds after which the client must send the ",(0,i.jsx)(n.code,{children:"refresh"})," command with new credentials to Centrifugo. Centrifugo clients must handle this ",(0,i.jsx)(n.code,{children:"ttl"})," field and automatically start the refresh process."]}),"\n",(0,i.jsxs)(n.p,{children:["For example, a Javascript browser client will send an AJAX POST request to your application when it's time to refresh credentials. By default, this request goes to ",(0,i.jsx)(n.code,{children:"/centrifuge/refresh"})," URL endpoint. In response your server must return JSON with a new connection JWT:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'{\n "token": token\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["So you must just return the same connection JWT for your user when rendering the page initially. But with actual valid ",(0,i.jsx)(n.code,{children:"exp"}),". Javascript client will then send them to Centrifugo server and connection will be refreshed for a time you set in ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"In this case, you know which user wants to refresh its connection because this is just a general request to your app - so your session mechanism will tell you about the user."}),"\n",(0,i.jsx)(n.p,{children:"If you don't want to refresh the connection for this user - just return 403 Forbidden on refresh request to your application backend."}),"\n",(0,i.jsx)(n.p,{children:"Javascript client also has options to hook into a refresh mechanism to implement your custom way of refreshing. Other Centrifugo clients also should have hooks to refresh credentials but depending on client API for this can be different - see specific client docs."}),"\n",(0,i.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,i.jsx)(n.p,{children:"Let's look at how to generate connection HS256 JWT in Python:"}),"\n",(0,i.jsx)(n.h3,{id:"simplest-token",children:"Simplest token"}),"\n","\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\n\ntoken = jwt.encode({"sub": "42"}, "secret").decode()\n\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42' }, 'secret');\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsxs)(n.p,{children:["Note that we use the value of ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo config here (in this case ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," value is just ",(0,i.jsx)(n.code,{children:"secret"}),"). The only two who must know the HMAC secret key is your application backend which generates JWT and Centrifugo. You should never reveal the HMAC secret key to your users."]}),"\n",(0,i.jsx)(n.p,{children:"Then you can pass this token to your client side and use it when connecting to Centrifugo:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",metastring:'title="Using centrifuge-js v2.x"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket");\ncentrifuge.setToken(token);\ncentrifuge.connect();\n'})}),"\n",(0,i.jsx)(n.h3,{id:"token-with-expiration",children:"Token with expiration"}),"\n",(0,i.jsx)(n.p,{children:"HS256 token that will be valid for 5 minutes:"}),"\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {"sub": "42", "exp": int(time.time()) + 5*60}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42' }, 'secret', { expiresIn: 5 * 60 });\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsx)(n.h3,{id:"token-with-additional-connection-info",children:"Token with additional connection info"}),"\n",(0,i.jsx)(n.p,{children:"Let's attach user name:"}),"\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\n\nclaims = {"sub": "42", "info": {"name": "Alexander Emelin"}}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42', info: {\"name\": \"Alexander Emelin\"} }, 'secret');\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsx)(n.h3,{id:"investigating-problems-with-jwt",children:"Investigating problems with JWT"}),"\n",(0,i.jsxs)(n.p,{children:["You can use ",(0,i.jsx)(n.a,{href:"https://jwt.io/",children:"jwt.io"})," site to investigate the contents of your tokens. Also, server logs usually contain some useful information."]}),"\n",(0,i.jsx)(n.h2,{id:"json-web-key-support",children:"JSON Web Key support"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo supports JSON Web Key (JWK) ",(0,i.jsx)(n.a,{href:"https://tools.ietf.org/html/rfc7517",children:"spec"}),". This means that it's possible to improve JWT security by providing an endpoint to Centrifugo from where to load JWK (by looking at ",(0,i.jsx)(n.code,{children:"kid"})," header of JWT)."]}),"\n",(0,i.jsxs)(n.p,{children:["A mechanism can be enabled by providing ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," string option to Centrifugo (HTTP address)."]}),"\n",(0,i.jsxs)(n.p,{children:["As soon as ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," set all tokens will be verified using JSON Web Key Set loaded from JWKS endpoint. This makes it impossible to use non-JWK based tokens to connect and subscribe to private channels."]}),"\n",(0,i.jsx)(n.p,{children:"At the moment Centrifugo caches keys loaded from an endpoint for one hour."}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo will load keys from JWKS endpoint by issuing GET HTTP request with 1 second timeout and one retry in case of failure (not configurable at the moment)."}),"\n",(0,i.jsxs)(n.p,{children:["Only ",(0,i.jsx)(n.code,{children:"RSA"})," algorithm is supported."]}),"\n",(0,i.jsx)(n.p,{children:"JWKS support enabled both connection and private channel subscription tokens."})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},32691:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/diagram_jwt_authentication-6a769cc8f218228df5954d240b2057cc.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/211f1d7a.afce88de.js b/assets/js/211f1d7a.afce88de.js new file mode 100644 index 000000000..f83b33247 --- /dev/null +++ b/assets/js/211f1d7a.afce88de.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2540],{70733:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var i=t(85893),s=t(11151),o=t(74866),r=t(85162);const a={id:"authentication",title:"Client authentication"},c=void 0,l={id:"server/authentication",title:"Client authentication",description:"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.",source:"@site/versioned_docs/version-3/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/3/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/server/authentication.md",tags:[],version:"3",frontMatter:{id:"authentication",title:"Client authentication"},sidebar:"Guides",previous:{title:"Server API",permalink:"/docs/3/server/server_api"},next:{title:"Channels",permalink:"/docs/3/server/channels"}},d={},h=[{value:"Claims",id:"claims",level:2},{value:"sub",id:"sub",level:3},{value:"exp",id:"exp",level:3},{value:"iat",id:"iat",level:3},{value:"jti",id:"jti",level:3},{value:"aud",id:"aud",level:3},{value:"iss",id:"iss",level:3},{value:"info",id:"info",level:3},{value:"b64info",id:"b64info",level:3},{value:"channels",id:"channels",level:3},{value:"subs",id:"subs",level:3},{value:"Subscribe options:",id:"subscribe-options",level:4},{value:"Override object",id:"override-object",level:4},{value:"meta",id:"meta",level:3},{value:"expire_at",id:"expire_at",level:3},{value:"Connection expiration",id:"connection-expiration",level:2},{value:"Examples",id:"examples",level:2},{value:"Simplest token",id:"simplest-token",level:3},{value:"Token with expiration",id:"token-with-expiration",level:3},{value:"Token with additional connection info",id:"token-with-additional-connection-info",level:3},{value:"Investigating problems with JWT",id:"investigating-problems-with-jwt",level:3},{value:"JSON Web Key support",id:"json-web-key-support",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["To authenticate incoming connection (client) Centrifugo can use ",(0,i.jsx)(n.a,{href:"https://jwt.io/introduction",children:"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."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["If you prefer to avoid using JWT then look at ",(0,i.jsx)(n.a,{href:"/docs/3/server/proxy",children:"the proxy feature"}),". It allows proxying connection requests from Centrifugo to your application backend for authentication details."]})}),"\n",(0,i.jsxs)(n.p,{children:["Upon connecting to Centrifugo client should provide a connection JWT with several predefined credential claims. If you've never heard about JWT before - refer to ",(0,i.jsx)(n.a,{href:"https://jwt.io/",children:"jwt.io"})," page."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{src:t(95718).Z+"",width:"2600",height:"906"})}),"\n",(0,i.jsx)(n.p,{children:"At the moment Centrifugo supports HMAC, RSA and ECDSA JWT algorithms - i.e. HS256, HS384, HS512, RSA256, RSA384, RSA512, EC256, EC384, EC512."}),"\n",(0,i.jsxs)(n.p,{children:["We will use Javascript Centrifugo client here for example snippets for client-side and ",(0,i.jsx)(n.a,{href:"https://github.com/jpadilla/pyjwt",children:"PyJWT"})," Python library to generate a connection token on the backend side."]}),"\n",(0,i.jsxs)(n.p,{children:["To add HMAC secret key to Centrifugo add ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," to configuration file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_hmac_secret_key": "<YOUR-SECRET-STRING-HERE>"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To add RSA public key (must be PEM encoded string) add ",(0,i.jsx)(n.code,{children:"token_rsa_public_key"})," option, ex:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_rsa_public_key": "-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZ..."\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To add ECDSA public key (must be PEM encoded string) add ",(0,i.jsx)(n.code,{children:"token_ecdsa_public_key"})," option, ex:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_ecdsa_public_key": "-----BEGIN PUBLIC KEY-----\\nxyz23adf..."\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"claims",children:"Claims"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo uses the following claims in a JWT: ",(0,i.jsx)(n.code,{children:"sub"}),", ",(0,i.jsx)(n.code,{children:"exp"}),", ",(0,i.jsx)(n.code,{children:"iat"}),", ",(0,i.jsx)(n.code,{children:"jti"}),", ",(0,i.jsx)(n.code,{children:"info"}),", ",(0,i.jsx)(n.code,{children:"b64info"}),", ",(0,i.jsx)(n.code,{children:"channels"}),", ",(0,i.jsx)(n.code,{children:"subs"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,i.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,i.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["If a user is not currently authenticated in an application, but you want to let him connect to Centrifugo anyway \u2013 you can use an empty string as a user ID in ",(0,i.jsx)(n.code,{children:"sub"})," claim. This is called anonymous access. In this case, the ",(0,i.jsx)(n.code,{children:"anonymous"})," option must be enabled in Centrifugo configuration for channels that the client will subscribe to."]}),"\n",(0,i.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,i.jsx)(n.p,{children:"This is a UNIX timestamp seconds when the token will expire. This is a standard JWT claim - all JWT libraries for different languages provide an API to set it."}),"\n",(0,i.jsxs)(n.p,{children:["If ",(0,i.jsx)(n.code,{children:"exp"})," claim is not provided then Centrifugo won't expire connection. When provided special algorithm will find connections with ",(0,i.jsx)(n.code,{children:"exp"})," in the past and activate the connection refresh mechanism. Refresh mechanism allows connection to survive and be prolonged. In case of refresh failure, the client connection will be eventually closed by Centrifugo and won't be accepted until new valid and actual credentials are provided in the connection token."]}),"\n",(0,i.jsx)(n.p,{children:"You can use the connection expiration mechanism in cases when you don't want users of your app to be subscribed on channels after being banned/deactivated in the application. Or to protect your users from token leakage (providing a reasonably short time of expiration)."}),"\n",(0,i.jsxs)(n.p,{children:["Choose ",(0,i.jsx)(n.code,{children:"exp"})," value wisely, you don't need small values because the refresh mechanism will hit your application often with refresh requests. But setting this value too large can lead to slow user connection deactivation. This is a trade-off."]}),"\n",(0,i.jsxs)(n.p,{children:["Read more about connection expiration ",(0,i.jsx)(n.a,{href:"#connection-expiration",children:"below"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,i.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6",children:"definition in RFC"}),". This claim is optional but can be useful together with ",(0,i.jsx)(n.a,{href:"/docs/3/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,i.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7",children:"definition in RFC"}),". This claim is optional but can be useful together with ",(0,i.jsx)(n.a,{href:"/docs/3/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,i.jsx)(n.p,{children:"Handled since Centrifugo v3.2.0"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim)."]}),"\n",(0,i.jsxs)(n.p,{children:["But you can force this check by setting ",(0,i.jsx)(n.code,{children:"token_audience"})," string option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_audience": "centrifugo"\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_audience"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/3/server/private_channels",children:"private channels"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,i.jsx)(n.p,{children:"Handled since Centrifugo v3.2.0"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim)."]}),"\n",(0,i.jsxs)(n.p,{children:["But you can force this check by setting ",(0,i.jsx)(n.code,{children:"token_issuer"})," string option:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_issuer": "my_app"\n}\n'})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_issuer"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/3/server/private_channels",children:"private channels"}),")."]})}),"\n",(0,i.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,i.jsx)(n.p,{children:"This claim is optional - this is additional information about client connection that can be provided for Centrifugo. This information will be included in presence information, join/leave events, and channel publication if it was published from a client-side."}),"\n",(0,i.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,i.jsx)(n.p,{children:"If you are using binary Protobuf protocol you may want info to be custom bytes. Use this field in this case."}),"\n",(0,i.jsxs)(n.p,{children:["This field contains a ",(0,i.jsx)(n.code,{children:"base64"})," representation of your bytes. After receiving Centrifugo will decode base64 back to bytes and will embed the result into various places described above."]}),"\n",(0,i.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,i.jsxs)(n.p,{children:["An optional array of strings with server-side channels to subscribe a client to. See more details about ",(0,i.jsx)(n.a,{href:"/docs/3/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["By providing a list of channels in JWT with ",(0,i.jsx)(n.code,{children:"channels"})," claim you are not making them automatically unaccessible by other users. Other users can still call a client-side ",(0,i.jsx)(n.code,{children:".subscribe()"})," method and subscribe to these channels if channel permissions allow doing this. If you need to protect channels from being subscribed by other connections then you can use private channels inside this ",(0,i.jsx)(n.code,{children:"channels"})," array (i.e. starting with ",(0,i.jsx)(n.code,{children:"$"}),") or turn on ",(0,i.jsx)(n.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option for channels namespaces."]})}),"\n",(0,i.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,i.jsxs)(n.p,{children:["An optional map of channels with options. This is like a ",(0,i.jsx)(n.code,{children:"channels"})," claim but allows more control over server-side subscription since every channel can be annotated with info, data, and so on using options."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["This claim is called ",(0,i.jsx)(n.code,{children:"subs"})," as a shortcut from subscriptions. The claim ",(0,i.jsx)(n.code,{children:"sub"})," described above is a standart JWT claim to provide a user ID (it's a shortcut from subject). While claims have similar names they have different purpose in a connection JWT."]})}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["By providing a map of channels in JWT with ",(0,i.jsx)(n.code,{children:"subs"})," claim you are not making channels automatically unaccessible by other users. Other users can still call a client-side ",(0,i.jsx)(n.code,{children:".subscribe()"})," method and subscribe to these channels if channel permissions allow doing this. If you need to protect channels from being subscribed by other connections then you can use private channels inside this ",(0,i.jsx)(n.code,{children:"subs"})," map (i.e. starting with ",(0,i.jsx)(n.code,{children:"$"}),") or turn on ",(0,i.jsx)(n.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option for channels namespaces."]})}),"\n",(0,i.jsx)(n.p,{children:"Example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "subs": {\n "channel1": {\n "data": {"welcome": "welcome to channel1"}\n },\n "channel2": {\n "data": {"welcome": "welcome to channel2"}\n }\n }\n}\n'})}),"\n",(0,i.jsx)(n.h4,{id:"subscribe-options",children:"Subscribe options:"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Optional"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"info"}),(0,i.jsx)(n.td,{children:"JSON object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom channel info"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"b64info"}),(0,i.jsx)(n.td,{children:"string"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom channel info in Base64 - to pass binary channel info"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"data"}),(0,i.jsx)(n.td,{children:"JSON object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Custom JSON data to return in subscription context inside Connect reply"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"b64data"}),(0,i.jsx)(n.td,{children:"string"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsxs)(n.td,{children:["Same as ",(0,i.jsx)(n.code,{children:"data"})," but in Base64 to send binary data"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"override"}),(0,i.jsx)(n.td,{children:"Override object"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,i.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Field"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Optional"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"presence"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override presence"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"join_leave"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override join_leave"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"position"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override position"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"recover"}),(0,i.jsx)(n.td,{children:"BoolValue"}),(0,i.jsx)(n.td,{children:"yes"}),(0,i.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"meta",children:"meta"}),"\n",(0,i.jsxs)(n.p,{children:["Meta is an additional JSON object (ex. ",(0,i.jsx)(n.code,{children:'{"key": "value"}'}),") that will be attached to a connection. Unlike ",(0,i.jsx)(n.code,{children:"info"})," it's never exposed to clients and only accessible on a backend side. It will be included in proxy calls from Centrifugo to the application backend. Also, there is a ",(0,i.jsx)(n.code,{children:"get_user_connections"})," API method in Centrifugo PRO that returns this data in the user connection object."]}),"\n",(0,i.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,i.jsxs)(n.p,{children:["By default, Centrifugo looks on ",(0,i.jsx)(n.code,{children:"exp"})," claim to configure connection expiration. In most cases this is fine, but there could be situations where you wish to decouple token expiration check with connection expiration time. As soon as the ",(0,i.jsx)(n.code,{children:"expire_at"})," claim is provided (set) in JWT Centrifugo relies on it for setting connection expiration time (JWT expiration still checked over ",(0,i.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the connection should expire."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Set it to the future time for expiring connection at some point"}),"\n",(0,i.jsxs)(n.li,{children:["Set it to ",(0,i.jsx)(n.code,{children:"0"})," to disable connection expiration (but still check token ",(0,i.jsx)(n.code,{children:"exp"})," claim)."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,i.jsxs)(n.p,{children:["As said above ",(0,i.jsx)(n.code,{children:"exp"})," claim in a connection token allows expiring client connection at some point in time. Let's look in detail at what happens when Centrifugo detects that the connection is going to expire."]}),"\n",(0,i.jsx)(n.p,{children:"First, you should do is enable client expiration mechanism in Centrifugo providing a connection JWT with expiration:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\ntoken = jwt.encode({"sub": "42", "exp": int(time.time()) + 10*60}, "secret").decode()\n\nprint(token)\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Let's suppose that you set ",(0,i.jsx)(n.code,{children:"exp"})," field to timestamp that will expire in 10 minutes and the client connected to Centrifugo with this token. During 10 minutes the connection will be kept by Centrifugo. When this time passed Centrifugo gives the connection some time (configured, 25 seconds by default) to refresh its credentials and provide a new valid token with new ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When a client first connects to Centrifugo it receives the ",(0,i.jsx)(n.code,{children:"ttl"})," value in connect reply. That ",(0,i.jsx)(n.code,{children:"ttl"})," value contains the number of seconds after which the client must send the ",(0,i.jsx)(n.code,{children:"refresh"})," command with new credentials to Centrifugo. Centrifugo clients must handle this ",(0,i.jsx)(n.code,{children:"ttl"})," field and automatically start the refresh process."]}),"\n",(0,i.jsxs)(n.p,{children:["For example, a Javascript browser client will send an AJAX POST request to your application when it's time to refresh credentials. By default, this request goes to ",(0,i.jsx)(n.code,{children:"/centrifuge/refresh"})," URL endpoint. In response your server must return JSON with a new connection JWT:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'{\n "token": token\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["So you must just return the same connection JWT for your user when rendering the page initially. But with actual valid ",(0,i.jsx)(n.code,{children:"exp"}),". Javascript client will then send them to Centrifugo server and connection will be refreshed for a time you set in ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"In this case, you know which user wants to refresh its connection because this is just a general request to your app - so your session mechanism will tell you about the user."}),"\n",(0,i.jsx)(n.p,{children:"If you don't want to refresh the connection for this user - just return 403 Forbidden on refresh request to your application backend."}),"\n",(0,i.jsx)(n.p,{children:"Javascript client also has options to hook into a refresh mechanism to implement your custom way of refreshing. Other Centrifugo clients also should have hooks to refresh credentials but depending on client API for this can be different - see specific client docs."}),"\n",(0,i.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,i.jsx)(n.p,{children:"Let's look at how to generate connection HS256 JWT in Python:"}),"\n",(0,i.jsx)(n.h3,{id:"simplest-token",children:"Simplest token"}),"\n","\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\n\ntoken = jwt.encode({"sub": "42"}, "secret").decode()\n\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42' }, 'secret');\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsxs)(n.p,{children:["Note that we use the value of ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo config here (in this case ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," value is just ",(0,i.jsx)(n.code,{children:"secret"}),"). The only two who must know the HMAC secret key is your application backend which generates JWT and Centrifugo. You should never reveal the HMAC secret key to your users."]}),"\n",(0,i.jsx)(n.p,{children:"Then you can pass this token to your client side and use it when connecting to Centrifugo:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",metastring:'title="Using centrifuge-js v2.x"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket");\ncentrifuge.setToken(token);\ncentrifuge.connect();\n'})}),"\n",(0,i.jsx)(n.h3,{id:"token-with-expiration",children:"Token with expiration"}),"\n",(0,i.jsx)(n.p,{children:"HS256 token that will be valid for 5 minutes:"}),"\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {"sub": "42", "exp": int(time.time()) + 5*60}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42' }, 'secret', { expiresIn: 5 * 60 });\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsx)(n.h3,{id:"token-with-additional-connection-info",children:"Token with additional connection info"}),"\n",(0,i.jsx)(n.p,{children:"Let's attach user name:"}),"\n",(0,i.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,i.jsx)(r.Z,{value:"python",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-python",children:'import jwt\n\nclaims = {"sub": "42", "info": {"name": "Alexander Emelin"}}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,i.jsx)(r.Z,{value:"node",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"var jwt = require('jsonwebtoken');\n\nvar token = jwt.sign({ sub: '42', info: {\"name\": \"Alexander Emelin\"} }, 'secret');\n\nconsole.log(token);\n"})})})]}),"\n",(0,i.jsx)(n.h3,{id:"investigating-problems-with-jwt",children:"Investigating problems with JWT"}),"\n",(0,i.jsxs)(n.p,{children:["You can use ",(0,i.jsx)(n.a,{href:"https://jwt.io/",children:"jwt.io"})," site to investigate the contents of your tokens. Also, server logs usually contain some useful information."]}),"\n",(0,i.jsx)(n.h2,{id:"json-web-key-support",children:"JSON Web Key support"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo supports JSON Web Key (JWK) ",(0,i.jsx)(n.a,{href:"https://tools.ietf.org/html/rfc7517",children:"spec"}),". This means that it's possible to improve JWT security by providing an endpoint to Centrifugo from where to load JWK (by looking at ",(0,i.jsx)(n.code,{children:"kid"})," header of JWT)."]}),"\n",(0,i.jsxs)(n.p,{children:["A mechanism can be enabled by providing ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," string option to Centrifugo (HTTP address)."]}),"\n",(0,i.jsxs)(n.p,{children:["As soon as ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," set all tokens will be verified using JSON Web Key Set loaded from JWKS endpoint. This makes it impossible to use non-JWK based tokens to connect and subscribe to private channels."]}),"\n",(0,i.jsx)(n.p,{children:"At the moment Centrifugo caches keys loaded from an endpoint for one hour."}),"\n",(0,i.jsx)(n.p,{children:"Centrifugo will load keys from JWKS endpoint by issuing GET HTTP request with 1 second timeout and one retry in case of failure (not configurable at the moment)."}),"\n",(0,i.jsxs)(n.p,{children:["Only ",(0,i.jsx)(n.code,{children:"RSA"})," algorithm is supported."]}),"\n",(0,i.jsx)(n.p,{children:"JWKS support enabled both connection and private channel subscription tokens."})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},85162:(e,n,t)=>{t.d(n,{Z:()=>r});t(67294);var i=t(36905);const s={tabItem:"tabItem_Ymn6"};var o=t(85893);function r(e){let{children:n,hidden:t,className:r}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,i.Z)(s.tabItem,r),hidden:t,children:n})}},74866:(e,n,t)=>{t.d(n,{Z:()=>w});var i=t(67294),s=t(36905),o=t(12466),r=t(16550),a=t(20469),c=t(91980),l=t(67392),d=t(50012);function h(e){return i.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,i.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:i,default:s}}=e;return{value:n,label:t,attributes:i,default:s}}))}(t);return function(e){const n=(0,l.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const s=(0,r.k6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,c._X)(o),(0,i.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(s.location.search);n.set(o,e),s.replace({...s.location,search:n.toString()})}),[o,s])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,o=u(e),[r,c]=(0,i.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const i=t.find((e=>e.default))??t[0];if(!i)throw new Error("Unexpected error: 0 tabValues");return i.value}({defaultValue:n,tabValues:o}))),[l,h]=x({queryString:t,groupId:s}),[j,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,o]=(0,d.Nk)(t);return[s,(0,i.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:s}),m=(()=>{const e=l??j;return p({value:e,tabValues:o})?e:null})();(0,a.Z)((()=>{m&&c(m)}),[m]);return{selectedValue:r,selectValue:(0,i.useCallback)((e=>{if(!p({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);c(e),h(e),f(e)}),[h,f,o]),tabValues:o}}var f=t(72389);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=t(85893);function g(e){let{className:n,block:t,selectedValue:i,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),d=e=>{const n=e.currentTarget,t=c.indexOf(n),s=a[t].value;s!==i&&(l(n),r(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=c.indexOf(e.currentTarget)+1;n=c[t]??c[0];break}case"ArrowLeft":{const t=c.indexOf(e.currentTarget)-1;n=c[t]??c[c.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:o}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...o,className:(0,s.Z)("tabs__item",m.tabItem,o?.className,{"tabs__item--active":i===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:s}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===s));return e?(0,i.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function y(e){const n=j(e);return(0,b.jsxs)("div",{className:(0,s.Z)("tabs-container",m.tabList),children:[(0,b.jsx)(g,{...n,...e}),(0,b.jsx)(v,{...n,...e})]})}function w(e){const n=(0,f.Z)();return(0,b.jsx)(y,{...e,children:h(e.children)},String(n))}},95718:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/diagram_jwt_authentication-6a769cc8f218228df5954d240b2057cc.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var i=t(67294);const s={},o=i.createContext(s);function r(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2312.a6b4f4c3.js b/assets/js/2312.a6b4f4c3.js new file mode 100644 index 000000000..15f2a291c --- /dev/null +++ b/assets/js/2312.a6b4f4c3.js @@ -0,0 +1 @@ +(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2312],{9286:(e,t,n)=>{"use strict";n.d(t,{Z:()=>V});var o=n(67294),s=n(72389),c=n(36905),r=n(92949),a=n(86668);function l(){const{prism:e}=(0,a.L)(),{colorMode:t}=(0,r.I)(),n=e.theme,o=e.darkTheme||n;return"dark"===t?o:n}var i=n(35281),u=n(87594),d=n.n(u);const m=/title=(?<quote>["'])(?<title>.*?)\1/,p=/\{(?<range>[\d,-]+)\}/,b={js:{start:"\\/\\/",end:""},jsBlock:{start:"\\/\\*",end:"\\*\\/"},jsx:{start:"\\{\\s*\\/\\*",end:"\\*\\/\\s*\\}"},bash:{start:"#",end:""},html:{start:"\x3c!--",end:"--\x3e"}},f={...b,lua:{start:"--",end:""},wasm:{start:"\\;\\;",end:""},tex:{start:"%",end:""},vb:{start:"['\u2018\u2019]",end:""},vbnet:{start:"(?:_\\s*)?['\u2018\u2019]",end:""},rem:{start:"[Rr][Ee][Mm]\\b",end:""},f90:{start:"!",end:""},ml:{start:"\\(\\*",end:"\\*\\)"},cobol:{start:"\\*>",end:""}},h=Object.keys(b);function g(e,t){const n=e.map((e=>{const{start:n,end:o}=f[e];return`(?:${n}\\s*(${t.flatMap((e=>[e.line,e.block?.start,e.block?.end].filter(Boolean))).join("|")})\\s*${o})`})).join("|");return new RegExp(`^\\s*(?:${n})\\s*$`)}function k(e,t){let n=e.replace(/\n$/,"");const{language:o,magicComments:s,metastring:c}=t;if(c&&p.test(c)){const e=c.match(p).groups.range;if(0===s.length)throw new Error(`A highlight range has been given in code block's metastring (\`\`\` ${c}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.`);const t=s[0].className,o=d()(e).filter((e=>e>0)).map((e=>[e-1,[t]]));return{lineClassNames:Object.fromEntries(o),code:n}}if(void 0===o)return{lineClassNames:{},code:n};const r=function(e,t){switch(e){case"js":case"javascript":case"ts":case"typescript":return g(["js","jsBlock"],t);case"jsx":case"tsx":return g(["js","jsBlock","jsx"],t);case"html":return g(["js","jsBlock","html"],t);case"python":case"py":case"bash":return g(["bash"],t);case"markdown":case"md":return g(["html","jsx","bash"],t);case"tex":case"latex":case"matlab":return g(["tex"],t);case"lua":case"haskell":case"sql":return g(["lua"],t);case"wasm":return g(["wasm"],t);case"vb":case"vba":case"visual-basic":return g(["vb","rem"],t);case"vbnet":return g(["vbnet","rem"],t);case"batch":return g(["rem"],t);case"basic":return g(["rem","f90"],t);case"fsharp":return g(["js","ml"],t);case"ocaml":case"sml":return g(["ml"],t);case"fortran":return g(["f90"],t);case"cobol":return g(["cobol"],t);default:return g(h,t)}}(o,s),a=n.split("\n"),l=Object.fromEntries(s.map((e=>[e.className,{start:0,range:""}]))),i=Object.fromEntries(s.filter((e=>e.line)).map((e=>{let{className:t,line:n}=e;return[n,t]}))),u=Object.fromEntries(s.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.start,t]}))),m=Object.fromEntries(s.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.end,t]})));for(let d=0;d<a.length;){const e=a[d].match(r);if(!e){d+=1;continue}const t=e.slice(1).find((e=>void 0!==e));i[t]?l[i[t]].range+=`${d},`:u[t]?l[u[t]].start=d:m[t]&&(l[m[t]].range+=`${l[m[t]].start}-${d-1},`),a.splice(d,1)}n=a.join("\n");const b={};return Object.entries(l).forEach((e=>{let[t,{range:n}]=e;d()(n).forEach((e=>{b[e]??=[],b[e].push(t)}))})),{lineClassNames:b,code:n}}const x={codeBlockContainer:"codeBlockContainer_Ckt0"};var B=n(85893);function j(e){let{as:t,...n}=e;const o=function(e){const t={color:"--prism-color",backgroundColor:"--prism-background-color"},n={};return Object.entries(e.plain).forEach((e=>{let[o,s]=e;const c=t[o];c&&"string"==typeof s&&(n[c]=s)})),n}(l());return(0,B.jsx)(t,{...n,style:o,className:(0,c.Z)(n.className,x.codeBlockContainer,i.k.common.codeBlock)})}const v={codeBlockContent:"codeBlockContent_biex",codeBlockTitle:"codeBlockTitle_Ktv7",codeBlock:"codeBlock_bY9V",codeBlockStandalone:"codeBlockStandalone_MEMb",codeBlockLines:"codeBlockLines_e6Vv",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_o6Pm",buttonGroup:"buttonGroup__atx"};function y(e){let{children:t,className:n}=e;return(0,B.jsx)(j,{as:"pre",tabIndex:0,className:(0,c.Z)(v.codeBlockStandalone,"thin-scrollbar",n),children:(0,B.jsx)("code",{className:v.codeBlockLines,children:t})})}var C=n(902);const N={attributes:!0,characterData:!0,childList:!0,subtree:!0};function w(e,t){const[n,s]=(0,o.useState)(),c=(0,o.useCallback)((()=>{s(e.current?.closest("[role=tabpanel][hidden]"))}),[e,s]);(0,o.useEffect)((()=>{c()}),[c]),function(e,t,n){void 0===n&&(n=N);const s=(0,C.zX)(t),c=(0,C.Ql)(n);(0,o.useEffect)((()=>{const t=new MutationObserver(s);return e&&t.observe(e,c),()=>t.disconnect()}),[e,s,c])}(n,(e=>{e.forEach((e=>{"attributes"===e.type&&"hidden"===e.attributeName&&(t(),c())}))}),{attributes:!0,characterData:!1,childList:!1,subtree:!1})}var L=n(14965);const E={codeLine:"codeLine_lJS_",codeLineNumber:"codeLineNumber_Tfdd",codeLineContent:"codeLineContent_feaV"};function I(e){let{line:t,classNames:n,showLineNumbers:o,getLineProps:s,getTokenProps:r}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const a=s({line:t,className:(0,c.Z)(n,o&&E.codeLine)}),l=t.map(((e,t)=>(0,B.jsx)("span",{...r({token:e})},t)));return(0,B.jsxs)("span",{...a,children:[o?(0,B.jsxs)(B.Fragment,{children:[(0,B.jsx)("span",{className:E.codeLineNumber}),(0,B.jsx)("span",{className:E.codeLineContent,children:l})]}):l,(0,B.jsx)("br",{})]})}var S=n(95999);function _(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"})})}function A(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"})})}const T={copyButtonCopied:"copyButtonCopied_obH4",copyButtonIcons:"copyButtonIcons_eSgA",copyButtonIcon:"copyButtonIcon_y97N",copyButtonSuccessIcon:"copyButtonSuccessIcon_LjdS"};function $(e){let{code:t,className:n}=e;const[s,r]=(0,o.useState)(!1),a=(0,o.useRef)(void 0),l=(0,o.useCallback)((()=>{!function(e,t){let{target:n=document.body}=void 0===t?{}:t;if("string"!=typeof e)throw new TypeError(`Expected parameter \`text\` to be a \`string\`, got \`${typeof e}\`.`);const o=document.createElement("textarea"),s=document.activeElement;o.value=e,o.setAttribute("readonly",""),o.style.contain="strict",o.style.position="absolute",o.style.left="-9999px",o.style.fontSize="12pt";const c=document.getSelection(),r=c.rangeCount>0&&c.getRangeAt(0);n.append(o),o.select(),o.selectionStart=0,o.selectionEnd=e.length;let a=!1;try{a=document.execCommand("copy")}catch{}o.remove(),r&&(c.removeAllRanges(),c.addRange(r)),s&&s.focus()}(t),r(!0),a.current=window.setTimeout((()=>{r(!1)}),1e3)}),[t]);return(0,o.useEffect)((()=>()=>window.clearTimeout(a.current)),[]),(0,B.jsx)("button",{type:"button","aria-label":s?(0,S.I)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,S.I)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,S.I)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,c.Z)("clean-btn",n,T.copyButton,s&&T.copyButtonCopied),onClick:l,children:(0,B.jsxs)("span",{className:T.copyButtonIcons,"aria-hidden":"true",children:[(0,B.jsx)(_,{className:T.copyButtonIcon}),(0,B.jsx)(A,{className:T.copyButtonSuccessIcon})]})})}function W(e){return(0,B.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,B.jsx)("path",{fill:"currentColor",d:"M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"})})}const M={wordWrapButtonIcon:"wordWrapButtonIcon_Bwma",wordWrapButtonEnabled:"wordWrapButtonEnabled_EoeP"};function Z(e){let{className:t,onClick:n,isEnabled:o}=e;const s=(0,S.I)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return(0,B.jsx)("button",{type:"button",onClick:n,className:(0,c.Z)("clean-btn",t,o&&M.wordWrapButtonEnabled),"aria-label":s,title:s,children:(0,B.jsx)(W,{className:M.wordWrapButtonIcon,"aria-hidden":"true"})})}function H(e){let{children:t,className:n="",metastring:s,title:r,showLineNumbers:i,language:u}=e;const{prism:{defaultLanguage:d,magicComments:p}}=(0,a.L)(),b=function(e){return e?.toLowerCase()}(u??function(e){const t=e.split(" ").find((e=>e.startsWith("language-")));return t?.replace(/language-/,"")}(n)??d),f=l(),h=function(){const[e,t]=(0,o.useState)(!1),[n,s]=(0,o.useState)(!1),c=(0,o.useRef)(null),r=(0,o.useCallback)((()=>{const n=c.current.querySelector("code");e?n.removeAttribute("style"):(n.style.whiteSpace="pre-wrap",n.style.overflowWrap="anywhere"),t((e=>!e))}),[c,e]),a=(0,o.useCallback)((()=>{const{scrollWidth:e,clientWidth:t}=c.current,n=e>t||c.current.querySelector("code").hasAttribute("style");s(n)}),[c]);return w(c,a),(0,o.useEffect)((()=>{a()}),[e,a]),(0,o.useEffect)((()=>(window.addEventListener("resize",a,{passive:!0}),()=>{window.removeEventListener("resize",a)})),[a]),{codeBlockRef:c,isEnabled:e,isCodeScrollable:n,toggle:r}}(),g=function(e){return e?.match(m)?.groups.title??""}(s)||r,{lineClassNames:x,code:y}=k(t,{metastring:s,language:b,magicComments:p}),C=i??function(e){return Boolean(e?.includes("showLineNumbers"))}(s);return(0,B.jsxs)(j,{as:"div",className:(0,c.Z)(n,b&&!n.includes(`language-${b}`)&&`language-${b}`),children:[g&&(0,B.jsx)("div",{className:v.codeBlockTitle,children:g}),(0,B.jsxs)("div",{className:v.codeBlockContent,children:[(0,B.jsx)(L.y$,{theme:f,code:y,language:b??"text",children:e=>{let{className:t,style:n,tokens:o,getLineProps:s,getTokenProps:r}=e;return(0,B.jsx)("pre",{tabIndex:0,ref:h.codeBlockRef,className:(0,c.Z)(t,v.codeBlock,"thin-scrollbar"),style:n,children:(0,B.jsx)("code",{className:(0,c.Z)(v.codeBlockLines,C&&v.codeBlockLinesWithNumbering),children:o.map(((e,t)=>(0,B.jsx)(I,{line:e,getLineProps:s,getTokenProps:r,classNames:x[t],showLineNumbers:C},t)))})})}}),(0,B.jsxs)("div",{className:v.buttonGroup,children:[(h.isEnabled||h.isCodeScrollable)&&(0,B.jsx)(Z,{className:v.codeButton,onClick:()=>h.toggle(),isEnabled:h.isEnabled}),(0,B.jsx)($,{className:v.codeButton,code:y})]})]})]})}function V(e){let{children:t,...n}=e;const c=(0,s.Z)(),r=function(e){return o.Children.toArray(e).some((e=>(0,o.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),a="string"==typeof r?H:y;return(0,B.jsx)(a,{...n,children:r},String(c))}},87594:(e,t)=>{function n(e){let t,n=[];for(let o of e.split(",").map((e=>e.trim())))if(/^-?\d+$/.test(o))n.push(parseInt(o,10));else if(t=o.match(/^(-?\d+)(-|\.\.\.?|\u2025|\u2026|\u22EF)(-?\d+)$/)){let[e,o,s,c]=t;if(o&&c){o=parseInt(o),c=parseInt(c);const e=o<c?1:-1;"-"!==s&&".."!==s&&"\u2025"!==s||(c+=e);for(let t=o;t!==c;t+=e)n.push(t)}}return n}t.default=n,e.exports=n},11151:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a,a:()=>r});var o=n(67294);const s={},c=o.createContext(s);function r(e){const t=o.useContext(c);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),o.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2391cf3d.0da3dbbb.js b/assets/js/2391cf3d.558435a6.js similarity index 99% rename from assets/js/2391cf3d.0da3dbbb.js rename to assets/js/2391cf3d.558435a6.js index 1fb27f6e0..c922d8110 100644 --- a/assets/js/2391cf3d.0da3dbbb.js +++ b/assets/js/2391cf3d.558435a6.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9523],{66796:(C,V,l)=>{l.r(V),l.d(V,{default:()=>a});l(67294);var H=l(85893);function a(){return(0,H.jsxs)("svg",{width:"160",height:"29",viewBox:"0 0 160 29",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,H.jsx)("path",{d:"M44.985 9.6615C44.7884 9.31867 44.5534 8.99925 44.2847 8.70943C43.7645 8.15179 43.138 7.70383 42.4421 7.39193C41.7311 7.06577 40.958 6.89714 40.1757 6.89752C39.0539 6.87224 37.9496 7.17767 37.0002 7.77581C36.0334 8.43321 35.2941 9.37419 34.8842 10.4692C34.3411 11.903 34.0845 13.4295 34.1287 14.9621V15.9572C34.0844 17.4882 34.3303 19.0138 34.8535 20.4533C35.2458 21.5357 35.9649 22.4693 36.9112 23.1251C37.8605 23.7223 38.9655 24.0248 40.0866 23.9942C40.8739 23.998 41.6529 23.8337 42.3715 23.5121C43.0801 23.1988 43.7214 22.7514 44.2602 22.1946C44.5261 21.9186 44.7657 21.6184 44.9757 21.2979V23.5397H52.0392V7.35204H44.9757L44.985 9.6615ZM45.16 15.8251C45.1745 16.512 45.1033 17.1982 44.9481 17.8675C44.8566 18.318 44.6443 18.7351 44.3339 19.0743C44.198 19.2054 44.0373 19.308 43.8611 19.376C43.685 19.4441 43.497 19.4762 43.3082 19.4705C43.1233 19.4755 42.9391 19.4431 42.7671 19.375C42.595 19.3069 42.4384 19.2046 42.307 19.0743C41.9942 18.7392 41.7815 18.3234 41.6928 17.8736C41.5474 17.2009 41.4814 16.5132 41.4962 15.8251V14.9621C41.4809 14.2796 41.5502 13.5978 41.7021 12.9322C41.7888 12.4922 41.9961 12.0849 42.3008 11.7559C42.4329 11.6293 42.5891 11.5306 42.76 11.4652C42.9309 11.3998 43.1131 11.3691 43.2959 11.3752C43.6746 11.3626 44.0429 11.4993 44.3216 11.7559C44.6326 12.0826 44.8455 12.4902 44.9358 12.9322C45.0916 13.5972 45.1628 14.2792 45.1477 14.9621L45.16 15.8251Z",fill:"black"}),(0,H.jsx)("path",{d:"M69.0228 7.546C68.2385 7.10178 67.347 6.8819 66.4461 6.91034C65.5689 6.89745 64.6994 7.07449 63.8972 7.42934C63.1644 7.75789 62.5068 8.23345 61.9654 8.82662C61.6036 9.22935 61.2987 9.68 61.0594 10.1657V7.36485H54.2233V23.5525H61.2652V13.8479C61.2518 13.483 61.3244 13.1201 61.477 12.7884C61.5941 12.531 61.7884 12.3163 62.0329 12.1742C62.2633 12.0474 62.5224 11.9818 62.7854 11.9838C62.9678 11.9736 63.1503 12.0022 63.3207 12.0679C63.4912 12.1335 63.6457 12.2346 63.7742 12.3645C64.0467 12.7129 64.1753 13.1526 64.1335 13.5929V23.5586H71.1725V12.6839C71.2056 11.5894 71.0197 10.4994 70.6258 9.47772C70.3089 8.68225 69.7476 8.00798 69.0228 7.55215",fill:"black"}),(0,H.jsx)("path",{d:"M100.983 17.1711C101.01 17.847 100.839 18.5161 100.492 19.0966C100.348 19.306 100.154 19.4755 99.9277 19.5894C99.701 19.7033 99.4491 19.7576 99.1956 19.7476C98.7997 19.7649 98.4123 19.6286 98.1146 19.3669C97.792 19.0157 97.5786 18.5781 97.5004 18.1077C97.3466 17.322 97.2787 16.5219 97.2977 15.7215V14.9751C97.2977 13.5901 97.4544 12.6044 97.7738 12.0209C97.9086 11.7459 98.1208 11.5163 98.3843 11.3603C98.6478 11.2043 98.9511 11.1285 99.257 11.1424C99.5036 11.1258 99.7503 11.1737 99.9727 11.2814C100.195 11.3891 100.386 11.5529 100.525 11.7566C100.837 12.3067 100.986 12.934 100.955 13.5655V13.6516H108.172C108.142 12.3473 107.724 11.0814 106.972 10.0154C106.183 8.9613 105.105 8.15821 103.87 7.70291C102.349 7.13926 100.735 6.86831 99.1127 6.9044C97.4861 6.87585 95.8711 7.18219 94.3679 7.80428C93.0212 8.37295 91.8787 9.33669 91.0911 10.5682C90.2906 11.7967 89.8903 13.3382 89.8903 15.1932V15.6478C89.8903 17.4905 90.2782 19.0393 91.0542 20.2943C91.8216 21.5312 92.9505 22.5025 94.2881 23.0767C95.8095 23.7154 97.4477 24.0292 99.0974 23.9981C100.697 24.0273 102.289 23.7575 103.79 23.2026C105.063 22.7366 106.177 21.9187 107.002 20.8441C107.801 19.7537 108.241 18.4423 108.261 17.0911H100.983V17.1711Z",fill:"black"}),(0,H.jsx)("path",{d:"M124.443 7.54468C123.66 7.09528 122.769 6.87106 121.866 6.89672C120.99 6.88379 120.122 7.06084 119.321 7.41571C118.587 7.74266 117.929 8.21858 117.389 8.81315C117.123 9.10845 116.888 9.42931 116.686 9.77121V2.36688H109.644V23.5389H116.686V13.8342C116.672 13.4693 116.745 13.1065 116.897 12.7748C117.016 12.518 117.21 12.3037 117.453 12.1606C117.685 12.0332 117.945 11.9676 118.209 11.9701C118.391 11.9602 118.574 11.9889 118.744 12.0546C118.914 12.1202 119.069 12.2212 119.198 12.3509C119.468 12.7005 119.595 13.1395 119.554 13.5793V23.545H126.596V12.6703C126.628 11.5756 126.441 10.4855 126.046 9.4641C125.731 8.66752 125.169 7.99272 124.443 7.53853",fill:"black"}),(0,H.jsx)("path",{d:"M139.064 9.66154C138.868 9.31871 138.633 8.99929 138.364 8.70947C137.844 8.15183 137.218 7.70387 136.522 7.39197C135.815 7.07262 135.049 6.90827 134.274 6.90986C133.152 6.88457 132.047 7.19 131.098 7.78815C130.132 8.44644 129.393 9.38712 128.982 10.4815C128.439 11.9154 128.182 13.4418 128.227 14.9744V15.9695C128.182 17.5005 128.428 19.0261 128.951 20.4656C129.344 21.548 130.063 22.4816 131.009 23.1374C131.958 23.7347 133.063 24.0371 134.185 24.0065C134.972 24.0104 135.751 23.846 136.469 23.5244C137.178 23.2111 137.819 22.7637 138.358 22.2069C138.624 21.931 138.864 21.6308 139.074 21.3102V23.552H146.137V7.36438H139.074L139.064 9.66154ZM139.243 15.8251C139.256 16.5122 139.184 17.1984 139.028 17.8675C138.936 18.3181 138.724 18.7351 138.413 19.0744C138.277 19.2055 138.117 19.308 137.941 19.3761C137.764 19.4441 137.576 19.4763 137.388 19.4705C137.203 19.4756 137.019 19.4431 136.846 19.375C136.674 19.3069 136.518 19.2046 136.386 19.0744C136.074 18.7392 135.861 18.3234 135.772 17.8737C135.627 17.2009 135.561 16.5133 135.576 15.8251V14.9621C135.56 14.2796 135.63 13.5978 135.781 12.9322C135.868 12.4922 136.076 12.085 136.38 11.756C136.512 11.6294 136.669 11.5306 136.839 11.4652C137.01 11.3998 137.192 11.3692 137.375 11.3752C137.754 11.3626 138.122 11.4993 138.401 11.756C138.712 12.0827 138.925 12.4903 139.015 12.9322C139.172 13.5971 139.245 14.2791 139.23 14.9621L139.243 15.8251Z",fill:"black"}),(0,H.jsx)("path",{d:"M160 11.6973V7.36408H156.984V2.37973H149.945V7.36408H147.47V11.6973H149.927V18.7608C149.879 19.7987 150.153 20.826 150.71 21.7029C151.226 22.4493 151.963 23.0152 152.817 23.3214C153.798 23.6629 154.831 23.8282 155.869 23.8097C156.648 23.8187 157.426 23.7426 158.188 23.5825C158.813 23.4502 159.42 23.244 159.997 22.9683V19.1479C159.511 19.3103 159.002 19.3942 158.489 19.3966C158.092 19.4252 157.698 19.3168 157.371 19.0895C157.11 18.8807 156.981 18.5121 156.981 17.9839V11.6912L160 11.6973Z",fill:"black"}),(0,H.jsx)("path",{d:"M27.8915 0.00614816H27.6642C20.9416 0.00614816 17.674 10.0639 17.674 10.0639V2.34635H0V23.5582H7.06348V9.41598H10.8317V23.5582H18.411C18.411 23.5582 22.3052 6.77177 25.9506 8.02784C28.4075 8.94917 21.4791 23.5429 21.4791 23.5429H31.4877C31.4877 23.5429 32.8758 14.0379 32.8973 10.7488C33.2044 5.16248 32.323 0 27.8853 0",fill:"black"}),(0,H.jsx)("path",{d:"M77.4955 24.4947C77.5058 24.2161 77.4236 23.9419 77.262 23.7147C77.0874 23.4942 76.8496 23.3324 76.5803 23.251C76.2265 23.1414 75.8573 23.0896 75.487 23.0974L71.7219 7.36431H79.0525L81.2484 20.8586H80.5789L82.876 7.36431H90.0593L86.4969 23.0974C86.1031 23.0851 85.71 23.137 85.3329 23.251C85.0775 23.3265 84.8547 23.4855 84.7002 23.7025C84.559 23.9419 84.4907 24.2171 84.5037 24.4947V28.2169H77.4955V24.4947Z",fill:"black"})]})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9523],{51374:(C,V,l)=>{l.r(V),l.d(V,{default:()=>a});l(67294);var H=l(85893);function a(){return(0,H.jsxs)("svg",{width:"160",height:"29",viewBox:"0 0 160 29",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,H.jsx)("path",{d:"M44.985 9.6615C44.7884 9.31867 44.5534 8.99925 44.2847 8.70943C43.7645 8.15179 43.138 7.70383 42.4421 7.39193C41.7311 7.06577 40.958 6.89714 40.1757 6.89752C39.0539 6.87224 37.9496 7.17767 37.0002 7.77581C36.0334 8.43321 35.2941 9.37419 34.8842 10.4692C34.3411 11.903 34.0845 13.4295 34.1287 14.9621V15.9572C34.0844 17.4882 34.3303 19.0138 34.8535 20.4533C35.2458 21.5357 35.9649 22.4693 36.9112 23.1251C37.8605 23.7223 38.9655 24.0248 40.0866 23.9942C40.8739 23.998 41.6529 23.8337 42.3715 23.5121C43.0801 23.1988 43.7214 22.7514 44.2602 22.1946C44.5261 21.9186 44.7657 21.6184 44.9757 21.2979V23.5397H52.0392V7.35204H44.9757L44.985 9.6615ZM45.16 15.8251C45.1745 16.512 45.1033 17.1982 44.9481 17.8675C44.8566 18.318 44.6443 18.7351 44.3339 19.0743C44.198 19.2054 44.0373 19.308 43.8611 19.376C43.685 19.4441 43.497 19.4762 43.3082 19.4705C43.1233 19.4755 42.9391 19.4431 42.7671 19.375C42.595 19.3069 42.4384 19.2046 42.307 19.0743C41.9942 18.7392 41.7815 18.3234 41.6928 17.8736C41.5474 17.2009 41.4814 16.5132 41.4962 15.8251V14.9621C41.4809 14.2796 41.5502 13.5978 41.7021 12.9322C41.7888 12.4922 41.9961 12.0849 42.3008 11.7559C42.4329 11.6293 42.5891 11.5306 42.76 11.4652C42.9309 11.3998 43.1131 11.3691 43.2959 11.3752C43.6746 11.3626 44.0429 11.4993 44.3216 11.7559C44.6326 12.0826 44.8455 12.4902 44.9358 12.9322C45.0916 13.5972 45.1628 14.2792 45.1477 14.9621L45.16 15.8251Z",fill:"black"}),(0,H.jsx)("path",{d:"M69.0228 7.546C68.2385 7.10178 67.347 6.8819 66.4461 6.91034C65.5689 6.89745 64.6994 7.07449 63.8972 7.42934C63.1644 7.75789 62.5068 8.23345 61.9654 8.82662C61.6036 9.22935 61.2987 9.68 61.0594 10.1657V7.36485H54.2233V23.5525H61.2652V13.8479C61.2518 13.483 61.3244 13.1201 61.477 12.7884C61.5941 12.531 61.7884 12.3163 62.0329 12.1742C62.2633 12.0474 62.5224 11.9818 62.7854 11.9838C62.9678 11.9736 63.1503 12.0022 63.3207 12.0679C63.4912 12.1335 63.6457 12.2346 63.7742 12.3645C64.0467 12.7129 64.1753 13.1526 64.1335 13.5929V23.5586H71.1725V12.6839C71.2056 11.5894 71.0197 10.4994 70.6258 9.47772C70.3089 8.68225 69.7476 8.00798 69.0228 7.55215",fill:"black"}),(0,H.jsx)("path",{d:"M100.983 17.1711C101.01 17.847 100.839 18.5161 100.492 19.0966C100.348 19.306 100.154 19.4755 99.9277 19.5894C99.701 19.7033 99.4491 19.7576 99.1956 19.7476C98.7997 19.7649 98.4123 19.6286 98.1146 19.3669C97.792 19.0157 97.5786 18.5781 97.5004 18.1077C97.3466 17.322 97.2787 16.5219 97.2977 15.7215V14.9751C97.2977 13.5901 97.4544 12.6044 97.7738 12.0209C97.9086 11.7459 98.1208 11.5163 98.3843 11.3603C98.6478 11.2043 98.9511 11.1285 99.257 11.1424C99.5036 11.1258 99.7503 11.1737 99.9727 11.2814C100.195 11.3891 100.386 11.5529 100.525 11.7566C100.837 12.3067 100.986 12.934 100.955 13.5655V13.6516H108.172C108.142 12.3473 107.724 11.0814 106.972 10.0154C106.183 8.9613 105.105 8.15821 103.87 7.70291C102.349 7.13926 100.735 6.86831 99.1127 6.9044C97.4861 6.87585 95.8711 7.18219 94.3679 7.80428C93.0212 8.37295 91.8787 9.33669 91.0911 10.5682C90.2906 11.7967 89.8903 13.3382 89.8903 15.1932V15.6478C89.8903 17.4905 90.2782 19.0393 91.0542 20.2943C91.8216 21.5312 92.9505 22.5025 94.2881 23.0767C95.8095 23.7154 97.4477 24.0292 99.0974 23.9981C100.697 24.0273 102.289 23.7575 103.79 23.2026C105.063 22.7366 106.177 21.9187 107.002 20.8441C107.801 19.7537 108.241 18.4423 108.261 17.0911H100.983V17.1711Z",fill:"black"}),(0,H.jsx)("path",{d:"M124.443 7.54468C123.66 7.09528 122.769 6.87106 121.866 6.89672C120.99 6.88379 120.122 7.06084 119.321 7.41571C118.587 7.74266 117.929 8.21858 117.389 8.81315C117.123 9.10845 116.888 9.42931 116.686 9.77121V2.36688H109.644V23.5389H116.686V13.8342C116.672 13.4693 116.745 13.1065 116.897 12.7748C117.016 12.518 117.21 12.3037 117.453 12.1606C117.685 12.0332 117.945 11.9676 118.209 11.9701C118.391 11.9602 118.574 11.9889 118.744 12.0546C118.914 12.1202 119.069 12.2212 119.198 12.3509C119.468 12.7005 119.595 13.1395 119.554 13.5793V23.545H126.596V12.6703C126.628 11.5756 126.441 10.4855 126.046 9.4641C125.731 8.66752 125.169 7.99272 124.443 7.53853",fill:"black"}),(0,H.jsx)("path",{d:"M139.064 9.66154C138.868 9.31871 138.633 8.99929 138.364 8.70947C137.844 8.15183 137.218 7.70387 136.522 7.39197C135.815 7.07262 135.049 6.90827 134.274 6.90986C133.152 6.88457 132.047 7.19 131.098 7.78815C130.132 8.44644 129.393 9.38712 128.982 10.4815C128.439 11.9154 128.182 13.4418 128.227 14.9744V15.9695C128.182 17.5005 128.428 19.0261 128.951 20.4656C129.344 21.548 130.063 22.4816 131.009 23.1374C131.958 23.7347 133.063 24.0371 134.185 24.0065C134.972 24.0104 135.751 23.846 136.469 23.5244C137.178 23.2111 137.819 22.7637 138.358 22.2069C138.624 21.931 138.864 21.6308 139.074 21.3102V23.552H146.137V7.36438H139.074L139.064 9.66154ZM139.243 15.8251C139.256 16.5122 139.184 17.1984 139.028 17.8675C138.936 18.3181 138.724 18.7351 138.413 19.0744C138.277 19.2055 138.117 19.308 137.941 19.3761C137.764 19.4441 137.576 19.4763 137.388 19.4705C137.203 19.4756 137.019 19.4431 136.846 19.375C136.674 19.3069 136.518 19.2046 136.386 19.0744C136.074 18.7392 135.861 18.3234 135.772 17.8737C135.627 17.2009 135.561 16.5133 135.576 15.8251V14.9621C135.56 14.2796 135.63 13.5978 135.781 12.9322C135.868 12.4922 136.076 12.085 136.38 11.756C136.512 11.6294 136.669 11.5306 136.839 11.4652C137.01 11.3998 137.192 11.3692 137.375 11.3752C137.754 11.3626 138.122 11.4993 138.401 11.756C138.712 12.0827 138.925 12.4903 139.015 12.9322C139.172 13.5971 139.245 14.2791 139.23 14.9621L139.243 15.8251Z",fill:"black"}),(0,H.jsx)("path",{d:"M160 11.6973V7.36408H156.984V2.37973H149.945V7.36408H147.47V11.6973H149.927V18.7608C149.879 19.7987 150.153 20.826 150.71 21.7029C151.226 22.4493 151.963 23.0152 152.817 23.3214C153.798 23.6629 154.831 23.8282 155.869 23.8097C156.648 23.8187 157.426 23.7426 158.188 23.5825C158.813 23.4502 159.42 23.244 159.997 22.9683V19.1479C159.511 19.3103 159.002 19.3942 158.489 19.3966C158.092 19.4252 157.698 19.3168 157.371 19.0895C157.11 18.8807 156.981 18.5121 156.981 17.9839V11.6912L160 11.6973Z",fill:"black"}),(0,H.jsx)("path",{d:"M27.8915 0.00614816H27.6642C20.9416 0.00614816 17.674 10.0639 17.674 10.0639V2.34635H0V23.5582H7.06348V9.41598H10.8317V23.5582H18.411C18.411 23.5582 22.3052 6.77177 25.9506 8.02784C28.4075 8.94917 21.4791 23.5429 21.4791 23.5429H31.4877C31.4877 23.5429 32.8758 14.0379 32.8973 10.7488C33.2044 5.16248 32.323 0 27.8853 0",fill:"black"}),(0,H.jsx)("path",{d:"M77.4955 24.4947C77.5058 24.2161 77.4236 23.9419 77.262 23.7147C77.0874 23.4942 76.8496 23.3324 76.5803 23.251C76.2265 23.1414 75.8573 23.0896 75.487 23.0974L71.7219 7.36431H79.0525L81.2484 20.8586H80.5789L82.876 7.36431H90.0593L86.4969 23.0974C86.1031 23.0851 85.71 23.137 85.3329 23.251C85.0775 23.3265 84.8547 23.4855 84.7002 23.7025C84.559 23.9419 84.4907 24.2171 84.5037 24.4947V28.2169H77.4955V24.4947Z",fill:"black"})]})}}}]); \ No newline at end of file diff --git a/assets/js/267a22d2.fac79ce0.js b/assets/js/267a22d2.0b07620a.js similarity index 99% rename from assets/js/267a22d2.fac79ce0.js rename to assets/js/267a22d2.0b07620a.js index 1eb470e8d..d81b7fd97 100644 --- a/assets/js/267a22d2.fac79ce0.js +++ b/assets/js/267a22d2.0b07620a.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9668],{8441:(e,s,t)=>{t.r(s),t.d(s,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>i,metadata:()=>l,toc:()=>o});var n=t(85893),r=t(11151);const i={id:"user_status",title:"User status API"},a=void 0,l={id:"pro/user_status",title:"User status API",description:"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.",source:"@site/versioned_docs/version-4/pro/user_status.md",sourceDirName:"pro",slug:"/pro/user_status",permalink:"/docs/4/pro/user_status",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/user_status.md",tags:[],version:"4",frontMatter:{id:"user_status",title:"User status API"},sidebar:"Pro",previous:{title:"Push notification API",permalink:"/docs/4/pro/push_notifications"},next:{title:"Connections API",permalink:"/docs/4/pro/connections"}},d={},o=[{value:"Client-side status update RPC",id:"client-side-status-update-rpc",level:3},{value:"update_user_status server API",id:"update_user_status-server-api",level:3},{value:"Update user status params",id:"update-user-status-params",level:4},{value:"Update user status result",id:"update-user-status-result",level:4},{value:"get_user_status server API",id:"get_user_status-server-api",level:3},{value:"Get user status params",id:"get-user-status-params",level:4},{value:"Get user status result",id:"get-user-status-result",level:4},{value:"UserStatus",id:"userstatus",level:4},{value:"delete_user_status server API",id:"delete_user_status-server-api",level:3},{value:"Delete user status params",id:"delete-user-status-params",level:4},{value:"Delete user status result",id:"delete-user-status-result",level:4},{value:"Configuration",id:"configuration",level:3}];function u(e){const s={a:"a",admonition:"admonition",code:"code",h3:"h3",h4:"h4",img:"img",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(s.p,{children:"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."}),"\n",(0,n.jsx)(s.p,{children:"What if you want to get a specific user status based on its recent activity in application? You can create a personal channel with a presence enabled for each user. It will show that user has an active connection with a server. But this won't show whether user did some actions in an application recently or just left it open while not actually using it."}),"\n",(0,n.jsx)(s.p,{children:(0,n.jsx)(s.img,{alt:"user status",src:t(89877).Z+"",width:"4790",height:"835"})}),"\n",(0,n.jsx)(s.p,{children:"User status feature of Centrifugo PRO allows calling a special RPC method from a client side when a user makes a useful action in an application (clicks on buttons, uses a mouse \u2013 whatever means that user really uses application at the moment). This call sets a time of last user activity in Redis, and this information can then be queried over Centrifugo PRO server API."}),"\n",(0,n.jsx)(s.p,{children:"The feature can be useful for chat applications when you need to get online/activity status for a list of buddies (Centrifugo supports batch requests to user status information \u2013 i.e. ask for many users in one call)."}),"\n",(0,n.jsx)(s.h3,{id:"client-side-status-update-rpc",children:"Client-side status update RPC"}),"\n",(0,n.jsxs)(s.p,{children:["Centrifugo PRO provides a built-in RPC method of client API called ",(0,n.jsx)(s.code,{children:"update_user_status"}),". Call it with empty parameters from a client side whenever user performs a useful action that proves it's active status in your app. For example, in Javascript:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-javascript",children:"await centrifuge.rpc('update_user_status', {});\n"})}),"\n",(0,n.jsx)(s.admonition,{type:"note",children:(0,n.jsx)(s.p,{children:"Don't forget to debounce this method calls on a client side to avoid exposing RPC on every mouse move event for example."})}),"\n",(0,n.jsx)(s.p,{children:"This RPC call sets user's last active time value in Redis (with sharding and Cluster support). Information about active status will be kept in Redis for a configured time interval, then expire."}),"\n",(0,n.jsx)(s.h3,{id:"update_user_status-server-api",children:"update_user_status server API"}),"\n",(0,n.jsxs)(s.p,{children:["It's also possible to call ",(0,n.jsx)(s.code,{children:"update_user_status"})," using Centrifugo server API (for example if you want to force status during application development or you want to proxy status updates over your app backend when using unidirectional transports):"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "update_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.h4,{id:"update-user-status-params",children:"Update user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to update status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"update-user-status-result",children:"Update user status result"}),"\n",(0,n.jsx)(s.p,{children:"Empty object at the moment."}),"\n",(0,n.jsx)(s.h3,{id:"get_user_status-server-api",children:"get_user_status server API"}),"\n",(0,n.jsx)(s.p,{children:"Now on a backend side you have access to a bulk API to effectively get status of particular users."}),"\n",(0,n.jsx)(s.p,{children:"Call RPC method of server API (over HTTP or GRPC):"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "get_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.p,{children:"You should get a response like this:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",children:'{\n "result":{\n "statuses":[\n {\n "user":"42",\n "active":1627107289,\n "online":1627107289\n }\n ]\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"In case information about last status update time not available the response will be like this:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",children:'{\n "result":{\n "statuses":[\n {\n "user":"42"\n }\n ]\n }\n}\n'})}),"\n",(0,n.jsxs)(s.p,{children:["I.e. status object will present in a response but ",(0,n.jsx)(s.code,{children:"active"})," field won't be set for status object."]}),"\n",(0,n.jsxs)(s.p,{children:["Note that Centrifugo also maintains ",(0,n.jsx)(s.code,{children:"online"})," field inside user status object. This field updated periodically by Centrifugo itself while user has active connection with a server. So you can draw ",(0,n.jsx)(s.code,{children:"away"})," statuses in your application: i.e. when user connected (",(0,n.jsx)(s.code,{children:"online"})," time) but not using application for a long time (",(0,n.jsx)(s.code,{children:"active"})," time)."]}),"\n",(0,n.jsx)(s.h4,{id:"get-user-status-params",children:"Get user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to get status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"get-user-status-result",children:"Get user status result"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Field name"}),(0,n.jsx)(s.th,{children:"Field type"}),(0,n.jsx)(s.th,{children:"Optional"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"statuses"}),(0,n.jsx)(s.td,{children:"array of UserStatus"}),(0,n.jsx)(s.td,{children:"no"}),(0,n.jsx)(s.td,{children:"Statuses for each user in params (same order)"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"userstatus",children:"UserStatus"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Field name"}),(0,n.jsx)(s.th,{children:"Field type"}),(0,n.jsx)(s.th,{children:"Optional"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"user"}),(0,n.jsx)(s.td,{children:"string"}),(0,n.jsx)(s.td,{children:"no"}),(0,n.jsx)(s.td,{children:"User ID"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"active"}),(0,n.jsx)(s.td,{children:"integer"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"Last active time (Unix seconds)"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"online"}),(0,n.jsx)(s.td,{children:"integer"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"Last online time (Unix seconds)"})]})]})]}),"\n",(0,n.jsx)(s.h3,{id:"delete_user_status-server-api",children:"delete_user_status server API"}),"\n",(0,n.jsxs)(s.p,{children:["If you need to clear user status information for some reason there is a ",(0,n.jsx)(s.code,{children:"delete_user_status"})," server API call:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "delete_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.h4,{id:"delete-user-status-params",children:"Delete user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to delete status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"delete-user-status-result",children:"Delete user status result"}),"\n",(0,n.jsx)(s.p,{children:"Empty object at the moment."}),"\n",(0,n.jsx)(s.h3,{id:"configuration",children:"Configuration"}),"\n",(0,n.jsx)(s.p,{children:"To enable Redis user status feature:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "user_status": {\n "enabled": true,\n "redis_address": "127.0.0.1:6379"\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"Redis configuration for user status feature matches Centrifugo Redis engine configuration. So Centrifugo supports client-side consistent sharding to scale Redis, Redis Sentinel, Redis Cluster for user status feature too."}),"\n",(0,n.jsxs)(s.p,{children:["It's also possible to reuse Centrifugo Redis engine by setting ",(0,n.jsx)(s.code,{children:"use_redis_from_engine"})," option instead of custom throttling Redis address declaration, like this:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": "localhost:6379",\n "user_status": {\n "enabled": true,\n "use_redis_from_engine": true,\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"In this case Redis active status will simply connect to Redis instances configured for Centrifugo Redis engine."}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"expire_interval"})," is a ",(0,n.jsx)(s.a,{href:"/docs/4/server/configuration#setting-time-duration-options",children:"duration"})," for how long Redis keys will be kept for each user. Expiration time extended on every update. By default expiration time is 31 day. To set it to 1 day:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "user_status": {\n ...\n "expire_interval": "24h"\n }\n}\n'})})]})}function c(e={}){const{wrapper:s}={...(0,r.a)(),...e.components};return s?(0,n.jsx)(s,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},89877:(e,s,t)=>{t.d(s,{Z:()=>n});const n=t.p+"assets/images/user_status-f8ea87131a11792b032fb4fc4eb373c5.png"},11151:(e,s,t)=>{t.d(s,{Z:()=>l,a:()=>a});var n=t(67294);const r={},i=n.createContext(r);function a(e){const s=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(i.Provider,{value:s},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9668],{8441:(e,s,t)=>{t.r(s),t.d(s,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>i,metadata:()=>l,toc:()=>o});var n=t(85893),r=t(11151);const i={id:"user_status",title:"User status API"},a=void 0,l={id:"pro/user_status",title:"User status API",description:"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.",source:"@site/versioned_docs/version-4/pro/user_status.md",sourceDirName:"pro",slug:"/pro/user_status",permalink:"/docs/4/pro/user_status",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/user_status.md",tags:[],version:"4",frontMatter:{id:"user_status",title:"User status API"},sidebar:"Pro",previous:{title:"Push notification API",permalink:"/docs/4/pro/push_notifications"},next:{title:"Connections API",permalink:"/docs/4/pro/connections"}},d={},o=[{value:"Client-side status update RPC",id:"client-side-status-update-rpc",level:3},{value:"update_user_status server API",id:"update_user_status-server-api",level:3},{value:"Update user status params",id:"update-user-status-params",level:4},{value:"Update user status result",id:"update-user-status-result",level:4},{value:"get_user_status server API",id:"get_user_status-server-api",level:3},{value:"Get user status params",id:"get-user-status-params",level:4},{value:"Get user status result",id:"get-user-status-result",level:4},{value:"UserStatus",id:"userstatus",level:4},{value:"delete_user_status server API",id:"delete_user_status-server-api",level:3},{value:"Delete user status params",id:"delete-user-status-params",level:4},{value:"Delete user status result",id:"delete-user-status-result",level:4},{value:"Configuration",id:"configuration",level:3}];function u(e){const s={a:"a",admonition:"admonition",code:"code",h3:"h3",h4:"h4",img:"img",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(s.p,{children:"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."}),"\n",(0,n.jsx)(s.p,{children:"What if you want to get a specific user status based on its recent activity in application? You can create a personal channel with a presence enabled for each user. It will show that user has an active connection with a server. But this won't show whether user did some actions in an application recently or just left it open while not actually using it."}),"\n",(0,n.jsx)(s.p,{children:(0,n.jsx)(s.img,{alt:"user status",src:t(84921).Z+"",width:"4790",height:"835"})}),"\n",(0,n.jsx)(s.p,{children:"User status feature of Centrifugo PRO allows calling a special RPC method from a client side when a user makes a useful action in an application (clicks on buttons, uses a mouse \u2013 whatever means that user really uses application at the moment). This call sets a time of last user activity in Redis, and this information can then be queried over Centrifugo PRO server API."}),"\n",(0,n.jsx)(s.p,{children:"The feature can be useful for chat applications when you need to get online/activity status for a list of buddies (Centrifugo supports batch requests to user status information \u2013 i.e. ask for many users in one call)."}),"\n",(0,n.jsx)(s.h3,{id:"client-side-status-update-rpc",children:"Client-side status update RPC"}),"\n",(0,n.jsxs)(s.p,{children:["Centrifugo PRO provides a built-in RPC method of client API called ",(0,n.jsx)(s.code,{children:"update_user_status"}),". Call it with empty parameters from a client side whenever user performs a useful action that proves it's active status in your app. For example, in Javascript:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-javascript",children:"await centrifuge.rpc('update_user_status', {});\n"})}),"\n",(0,n.jsx)(s.admonition,{type:"note",children:(0,n.jsx)(s.p,{children:"Don't forget to debounce this method calls on a client side to avoid exposing RPC on every mouse move event for example."})}),"\n",(0,n.jsx)(s.p,{children:"This RPC call sets user's last active time value in Redis (with sharding and Cluster support). Information about active status will be kept in Redis for a configured time interval, then expire."}),"\n",(0,n.jsx)(s.h3,{id:"update_user_status-server-api",children:"update_user_status server API"}),"\n",(0,n.jsxs)(s.p,{children:["It's also possible to call ",(0,n.jsx)(s.code,{children:"update_user_status"})," using Centrifugo server API (for example if you want to force status during application development or you want to proxy status updates over your app backend when using unidirectional transports):"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "update_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.h4,{id:"update-user-status-params",children:"Update user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to update status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"update-user-status-result",children:"Update user status result"}),"\n",(0,n.jsx)(s.p,{children:"Empty object at the moment."}),"\n",(0,n.jsx)(s.h3,{id:"get_user_status-server-api",children:"get_user_status server API"}),"\n",(0,n.jsx)(s.p,{children:"Now on a backend side you have access to a bulk API to effectively get status of particular users."}),"\n",(0,n.jsx)(s.p,{children:"Call RPC method of server API (over HTTP or GRPC):"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "get_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.p,{children:"You should get a response like this:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",children:'{\n "result":{\n "statuses":[\n {\n "user":"42",\n "active":1627107289,\n "online":1627107289\n }\n ]\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"In case information about last status update time not available the response will be like this:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",children:'{\n "result":{\n "statuses":[\n {\n "user":"42"\n }\n ]\n }\n}\n'})}),"\n",(0,n.jsxs)(s.p,{children:["I.e. status object will present in a response but ",(0,n.jsx)(s.code,{children:"active"})," field won't be set for status object."]}),"\n",(0,n.jsxs)(s.p,{children:["Note that Centrifugo also maintains ",(0,n.jsx)(s.code,{children:"online"})," field inside user status object. This field updated periodically by Centrifugo itself while user has active connection with a server. So you can draw ",(0,n.jsx)(s.code,{children:"away"})," statuses in your application: i.e. when user connected (",(0,n.jsx)(s.code,{children:"online"})," time) but not using application for a long time (",(0,n.jsx)(s.code,{children:"active"})," time)."]}),"\n",(0,n.jsx)(s.h4,{id:"get-user-status-params",children:"Get user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to get status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"get-user-status-result",children:"Get user status result"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Field name"}),(0,n.jsx)(s.th,{children:"Field type"}),(0,n.jsx)(s.th,{children:"Optional"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"statuses"}),(0,n.jsx)(s.td,{children:"array of UserStatus"}),(0,n.jsx)(s.td,{children:"no"}),(0,n.jsx)(s.td,{children:"Statuses for each user in params (same order)"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"userstatus",children:"UserStatus"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Field name"}),(0,n.jsx)(s.th,{children:"Field type"}),(0,n.jsx)(s.th,{children:"Optional"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"user"}),(0,n.jsx)(s.td,{children:"string"}),(0,n.jsx)(s.td,{children:"no"}),(0,n.jsx)(s.td,{children:"User ID"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"active"}),(0,n.jsx)(s.td,{children:"integer"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"Last active time (Unix seconds)"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"online"}),(0,n.jsx)(s.td,{children:"integer"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"Last online time (Unix seconds)"})]})]})]}),"\n",(0,n.jsx)(s.h3,{id:"delete_user_status-server-api",children:"delete_user_status server API"}),"\n",(0,n.jsxs)(s.p,{children:["If you need to clear user status information for some reason there is a ",(0,n.jsx)(s.code,{children:"delete_user_status"})," server API call:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "Authorization: apikey <API_KEY>" \\\n --request POST \\\n --data \'{"method": "delete_user_status", "params": {"users": ["42"]}}\' \\\n http://localhost:8000/api\n'})}),"\n",(0,n.jsx)(s.h4,{id:"delete-user-status-params",children:"Delete user status params"}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Parameter name"}),(0,n.jsx)(s.th,{children:"Parameter type"}),(0,n.jsx)(s.th,{children:"Required"}),(0,n.jsx)(s.th,{children:"Description"})]})}),(0,n.jsx)(s.tbody,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"users"}),(0,n.jsx)(s.td,{children:"array of strings"}),(0,n.jsx)(s.td,{children:"yes"}),(0,n.jsx)(s.td,{children:"List of users to delete status for"})]})})]}),"\n",(0,n.jsx)(s.h4,{id:"delete-user-status-result",children:"Delete user status result"}),"\n",(0,n.jsx)(s.p,{children:"Empty object at the moment."}),"\n",(0,n.jsx)(s.h3,{id:"configuration",children:"Configuration"}),"\n",(0,n.jsx)(s.p,{children:"To enable Redis user status feature:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "user_status": {\n "enabled": true,\n "redis_address": "127.0.0.1:6379"\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"Redis configuration for user status feature matches Centrifugo Redis engine configuration. So Centrifugo supports client-side consistent sharding to scale Redis, Redis Sentinel, Redis Cluster for user status feature too."}),"\n",(0,n.jsxs)(s.p,{children:["It's also possible to reuse Centrifugo Redis engine by setting ",(0,n.jsx)(s.code,{children:"use_redis_from_engine"})," option instead of custom throttling Redis address declaration, like this:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "engine": "redis",\n "redis_address": "localhost:6379",\n "user_status": {\n "enabled": true,\n "use_redis_from_engine": true,\n }\n}\n'})}),"\n",(0,n.jsx)(s.p,{children:"In this case Redis active status will simply connect to Redis instances configured for Centrifugo Redis engine."}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"expire_interval"})," is a ",(0,n.jsx)(s.a,{href:"/docs/4/server/configuration#setting-time-duration-options",children:"duration"})," for how long Redis keys will be kept for each user. Expiration time extended on every update. By default expiration time is 31 day. To set it to 1 day:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "user_status": {\n ...\n "expire_interval": "24h"\n }\n}\n'})})]})}function c(e={}){const{wrapper:s}={...(0,r.a)(),...e.components};return s?(0,n.jsx)(s,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},84921:(e,s,t)=>{t.d(s,{Z:()=>n});const n=t.p+"assets/images/user_status-f8ea87131a11792b032fb4fc4eb373c5.png"},11151:(e,s,t)=>{t.d(s,{Z:()=>l,a:()=>a});var n=t(67294);const r={},i=n.createContext(r);function a(e){const s=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(i.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2a42cb18.39f6beee.js b/assets/js/2a42cb18.39f6beee.js deleted file mode 100644 index 00e3b9c44..000000000 --- a/assets/js/2a42cb18.39f6beee.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7115],{52309:(e,n,o)=>{o.d(n,{Z:()=>r});var i=o(67294),t=o(85893);class r extends i.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={config:null,output:"Here will be configuration for v4",logs:"Here will be log of changes made in your config"}}onClick(e){if(!this.state.config)return void alert("Provide a configuration");let n;try{n=JSON.parse(this.state.config)}catch{return void alert("Invalid JSON")}let o=[],i=[],t=function(e){let n="config top-level";return void 0!==e&&(n="namespace {"+e.name+"}"),n},r=function(e,r,s){i.push("`"+e+"` renamed to `"+r+"`");let a=t(s);void 0===s&&(s=n),void 0===s[r]&&void 0!==s[e]&&(s[r]=s[e],delete s[e],o.push("renamed "+e+" to "+r+" in "+a))},s=function(e,r){i.push("`"+e+"` removed");let s=t(r);void 0===r&&(r=n),void 0!==r[e]&&(delete r[e],o.push("removed "+e+" from "+s))},a=function(e,r,s){i.push("`"+e+"` is now required");let a=t(s);void 0===s&&(s=n),void 0===s[e]&&(s[e]=r,o.push("added "+e+" to "+a))};s("use_unlimited_history_by_default"),r("client_anonymous","allow_anonymous_connect_without_token");let c=n;if(a("allow_user_limited_channels",!0),!0===c.protected?s("protected"):(a("allow_subscribe_for_client",!0),r("anonymous","allow_subscribe_for_anonymous")),!0===c.publish&&(r("publish","allow_publish_for_client"),a("allow_publish_for_anonymous",!0)),!0===c.presence&&(!0===c.presence_disabled_for_client?s("presence_disabled_for_client"):(a("allow_presence_for_subscriber",!0),a("allow_presence_for_anonymous",!0))),void 0!==c.history_ttl&&void 0!==c.history_size&&(!0===c.history_disabled_for_client?s("history_disabled_for_client"):(a("allow_history_for_subscriber",!0),a("allow_history_for_anonymous",!0))),!0===c.position?r("position","force_positioning"):s("position"),!0===c.recover?r("recover","force_recovery"):s("recover"),!0===c.join_leave&&a("force_push_join_leave",!0),void 0!==n.namespaces){let e=[];for(let o of n.namespaces)a("allow_user_limited_channels",!0,o),!0===o.protected?s("protected",o):(a("allow_subscribe_for_client",!0,o),r("anonymous","allow_subscribe_for_anonymous",o)),!0===o.publish&&(r("publish","allow_publish_for_client",o),a("allow_publish_for_anonymous",!0,o)),!0===o.presence&&(!0===o.presence_disabled_for_client?s("presence_disabled_for_client",o):(a("allow_presence_for_subscriber",!0,o),a("allow_presence_for_anonymous",!0,o))),void 0!==o.history_ttl&&void 0!==o.history_size&&(!0===o.history_disabled_for_client?s("history_disabled_for_client",o):(a("allow_history_for_subscriber",!0,o),a("allow_history_for_anonymous",!0,o))),!0===o.position?r("position","force_positioning",o):s("position",o),!0===o.recover?r("recover","force_recovery",o):s("recover",o),!0===o.join_leave&&a("force_push_join_leave",!0),e.push(o);n.namespaces=e}this.setState({output:JSON.stringify(n,null,"\t")}),this.setState({logs:JSON.stringify(o,null,"\t")}),console.log(i.join("\n\n"))}onChange(e){this.setState({config:e.target.value})}render(){return(0,t.jsxs)("div",{children:[(0,t.jsx)("textarea",{onChange:this.onChange,placeholder:"Paste your Centrifugo v3 JSON config here...",style:{width:"100%",height:"300px",border:"2px solid #ccc"}}),(0,t.jsx)("button",{onClick:this.onClick,children:"Convert"}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.output}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.logs})]})}}},59453:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>c,toc:()=>d});var i=o(85893),t=o(11151),r=o(52309);const s={id:"migration_v4",title:"Migrating to v4"},a=void 0,c={id:"getting-started/migration_v4",title:"Migrating to v4",description:"Centrifugo v4 development was concentrated around two main things:",source:"@site/docs/getting-started/migration-v4.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v4",permalink:"/docs/getting-started/migration_v4",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/getting-started/migration-v4.md",tags:[],version:"current",frontMatter:{id:"migration_v4",title:"Migrating to v4"}},l={},d=[{value:"Client SDK migration",id:"client-sdk-migration",level:2},{value:"Unidirectional transport migration",id:"unidirectional-transport-migration",level:2},{value:"SockJS migration",id:"sockjs-migration",level:2},{value:"Channel ASCII enforced",id:"channel-ascii-enforced",level:2},{value:"Subscription token migration",id:"subscription-token-migration",level:2},{value:"User-limited channel migration",id:"user-limited-channel-migration",level:2},{value:"Namespace configuration migration",id:"namespace-configuration-migration",level:2},{value:"Proxy disconnect code changes",id:"proxy-disconnect-code-changes",level:2},{value:"Other configuration option changes",id:"other-configuration-option-changes",level:2},{value:"Server API changes",id:"server-api-changes",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"Centrifugo v4 development was concentrated around two main things:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"adopt a new generation of client protocol"}),"\n",(0,i.jsx)(n.li,{children:"make namespaces secure by default"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"These goals dictate most of backwards compatibility changes in v4."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"What we would like to emphasize is that even there are many backwards incompatible changes it should be possible to migrate to Centrifugo v4 server without changing your client-side code at all. And then gradually upgrade the client-side. Below we are giving all the tips to achieve this."})}),"\n",(0,i.jsx)(n.h2,{id:"client-sdk-migration",children:"Client SDK migration"}),"\n",(0,i.jsx)(n.p,{children:"New generation of client protocol requires using the latest versions of client SDKs. During the next several days we will release the following SDK versions which are compatible with Centrifugo v4:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"centrifuge-js >= v3.0.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-go >= v0.9.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-dart >= v0.9.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-swift >= v0.5.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-java >= v0.2.0"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["New client SDKs ",(0,i.jsx)(n.strong,{children:"support only new client protocol"})," \u2013 you can not connect to Centrifugo v3 with them."]}),"\n",(0,i.jsx)(n.p,{children:"If you have a production system where you want to upgrade Centrifugo from v3 to v4 then the plan is:"}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["If you are using private channels (starting with ",(0,i.jsx)(n.code,{children:"$"}),") or user-limited channels (containing ",(0,i.jsx)(n.code,{children:"#"}),") then carefully read about subscription token migration and user-limited channels migration below."]})}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Upgrade Centrifugo and its configuration to adopt changes in v4."}),"\n",(0,i.jsxs)(n.li,{children:["In Centrifugo v4 config turn on ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run Centrifugo v4 \u2013 all current clients should continue working with it."}),"\n",(0,i.jsxs)(n.li,{children:["Then on the client-side uprade client SDK version to the one which works with Centrifugo v4, adopt changes in SDK API dictated by our new ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api",children:"client SDK API spec"}),". ",(0,i.jsx)(n.strong,{children:"Important thing"})," \u2013 add ",(0,i.jsx)(n.code,{children:"?cf_protocol_version=v2"})," URL param to the connection endpoint to tell Centrifugo that modern generation of protocol is being used by the connection (otherwise, it assumes old protocol since we have ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option enabled)."]}),"\n",(0,i.jsxs)(n.li,{children:["As soon as all your clients migrated to use new protocol generation you can remove ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option from the server configuration."]}),"\n",(0,i.jsxs)(n.li,{children:["After that you can remove ",(0,i.jsx)(n.code,{children:"?cf_protocol_version=v2"})," from connection endpoint on the client-side."]}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"If you are using mobile client SDKs then most probably some time must pass while clients update their apps to use an updated Centrifugo SDK version."})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Starting from Centrifugo v4.1.1 it's possible to completely turn off client protocol v1 by setting ",(0,i.jsx)(n.code,{children:"disable_client_protocol_v1"})," boolean option to ",(0,i.jsx)(n.code,{children:"true"}),"."]})}),"\n",(0,i.jsx)(n.h2,{id:"unidirectional-transport-migration",children:"Unidirectional transport migration"}),"\n",(0,i.jsx)(n.p,{children:"Client protocol framing also changed in unidirectional transports. The good news is that Centrifugo v4 still supports previous format for unidirectional transports."}),"\n",(0,i.jsxs)(n.p,{children:["When you are enabling ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option described above you also make unidirectional transports to work over old protocol format. So your existing clients will continue working just fine with Centrifugo v4. Then the same steps to migrate described above can be applied to unidirectional transport case. The only difference that in unidirectional approach you are not using Centrifugo SDKs."]}),"\n",(0,i.jsx)(n.h2,{id:"sockjs-migration",children:"SockJS migration"}),"\n",(0,i.jsx)(n.p,{children:"SockJS is now DEPRECATED in Centrifugo. Centrifugo v4 may be the last release which supports it. We now offer our own bidirectional emulation layer on top of HTTP-streaming and EventSource. See additional information in Centrifugo v4 introduction post."}),"\n",(0,i.jsx)(n.h2,{id:"channel-ascii-enforced",children:"Channel ASCII enforced"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo v2 and v3 docs mentioned the fact that channels must contain only ASCII characters. But it was not actually enforced by a server. Now Centrifugo is more strict. If a channel has non-ASCII characters then the ",(0,i.jsx)(n.code,{children:"107: bad request"})," error will be returned to the client. Please reach us out if this behavior is not suitable for your use case \u2013 we can discuss the use case and think on a proper solution together."]}),"\n",(0,i.jsx)(n.h2,{id:"subscription-token-migration",children:"Subscription token migration"}),"\n",(0,i.jsxs)(n.p,{children:["Subscription token now requires ",(0,i.jsx)(n.code,{children:"sub"})," claim (current user ID) to be set."]}),"\n",(0,i.jsxs)(n.p,{children:["In most cases the only change which is required to smoothly migrate to v4 without breaking things is to add a boolean option ",(0,i.jsx)(n.code,{children:'"skip_user_check_in_subscription_token": true'})," to a Centrifugo v4 configuration. This skips the check of ",(0,i.jsx)(n.code,{children:"sub"})," claim to contain the current user ID set to a connection during authentication."]}),"\n",(0,i.jsxs)(n.p,{children:["After that start adding ",(0,i.jsx)(n.code,{children:"sub"})," claim (with current user ID) to subscription tokens. As soon as all subscription tokens in your system contain user ID in ",(0,i.jsx)(n.code,{children:"sub"})," claim you can remove the ",(0,i.jsx)(n.code,{children:"skip_user_check_in_subscription_token"})," from a server configuration."]}),"\n",(0,i.jsxs)(n.p,{children:["One more important note is that ",(0,i.jsx)(n.code,{children:"client"})," claim in subscription token in Centrifugo v4 only supported for backwards compatibility. It must not be included into new subscription tokens."]}),"\n",(0,i.jsxs)(n.p,{children:["It's worth mentioning that Centrifugo v4 does not allow subscribing on channels starting with ",(0,i.jsx)(n.code,{children:"$"})," without token even if namespace marked as available for subscribing using sth like ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," option. This is done to prevent potential security risk during v3 -> v4 migration when client previously not available to subscribe to channels starting with ",(0,i.jsx)(n.code,{children:"$"})," in any case may get permissions to do so."]}),"\n",(0,i.jsx)(n.h2,{id:"user-limited-channel-migration",children:"User-limited channel migration"}),"\n",(0,i.jsxs)(n.p,{children:["User-limited channel support should now be allowed over a separate channel namespace option ",(0,i.jsx)(n.code,{children:"allow_user_limited_channels"}),". See below the namespace option converter which takes this change into account."]}),"\n",(0,i.jsx)(n.h2,{id:"namespace-configuration-migration",children:"Namespace configuration migration"}),"\n",(0,i.jsxs)(n.p,{children:["In Centrifugo v4 namespace configuration options have been changed. Centrifugo now has ",(0,i.jsx)(n.code,{children:"secure by default"})," namespaces. First thing to do is to read the new docs about ",(0,i.jsx)(n.a,{href:"/docs/server/channels",children:"channels and namespaces"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Then you can use the following converter which will transform your old namespace configuration to a new one. This converter tries to keep backwards compatibility \u2013 i.e. it should be possible to deploy Centrifugo with namespace configuration from converter output and have the same behaviour as before regarding channel permissions. We believe that new option names should provide a more readable configuration and may help to reveal some potential security improvements in your namespace configuration \u2013 i.e. making it more strict and protective."}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Do not blindly deploy things to production \u2013 test your system first, go through the possible usage scenarios and/or test cases."})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"It's fully client-side: your data won't be sent anywhere."})}),"\n","\n",(0,i.jsx)(r.Z,{}),"\n",(0,i.jsx)(n.h2,{id:"proxy-disconnect-code-changes",children:"Proxy disconnect code changes"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"reconnect"})," flag from custom disconnect code is removed. Reconnect advice is now determined by disconnect code value. This allowed us avoiding using JSON in WebSocket CLOSE frame reason. See ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#return-custom-disconnect",children:"proxy docs"})," docs for more details."]}),"\n",(0,i.jsx)(n.h2,{id:"other-configuration-option-changes",children:"Other configuration option changes"}),"\n",(0,i.jsx)(n.p,{children:"Several other non-namespace related options have been renamed or removed:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"client_anonymous"})," option renamed to ",(0,i.jsx)(n.code,{children:"allow_anonymous_connect_without_token"})," \u2013 new name better describes the purpose of this option which was previously not clear. Converter above takes this into account."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"use_unlimited_history_by_default"})," option was removed. It was used to help migrating from Centrifugo v2 to v3."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"server-api-changes",children:"Server API changes"}),"\n",(0,i.jsxs)(n.p,{children:["The only breaking change is that ",(0,i.jsx)(n.code,{children:"user_connections"})," API method (which is available in Centrifugo PRO only) was renamed to ",(0,i.jsx)(n.code,{children:"connections"}),". The method is more generic now with a broader possibilities \u2013 so previous name does not match the current behavior."]})]})}function u(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},11151:(e,n,o)=>{o.d(n,{Z:()=>a,a:()=>s});var i=o(67294);const t={},r=i.createContext(t);function s(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2a42cb18.543d524d.js b/assets/js/2a42cb18.543d524d.js new file mode 100644 index 000000000..e3ad6d2c5 --- /dev/null +++ b/assets/js/2a42cb18.543d524d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7115],{59453:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>c,toc:()=>d});var i=o(85893),t=o(11151),r=o(5717);const s={id:"migration_v4",title:"Migrating to v4"},a=void 0,c={id:"getting-started/migration_v4",title:"Migrating to v4",description:"Centrifugo v4 development was concentrated around two main things:",source:"@site/docs/getting-started/migration-v4.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v4",permalink:"/docs/getting-started/migration_v4",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/getting-started/migration-v4.md",tags:[],version:"current",frontMatter:{id:"migration_v4",title:"Migrating to v4"}},l={},d=[{value:"Client SDK migration",id:"client-sdk-migration",level:2},{value:"Unidirectional transport migration",id:"unidirectional-transport-migration",level:2},{value:"SockJS migration",id:"sockjs-migration",level:2},{value:"Channel ASCII enforced",id:"channel-ascii-enforced",level:2},{value:"Subscription token migration",id:"subscription-token-migration",level:2},{value:"User-limited channel migration",id:"user-limited-channel-migration",level:2},{value:"Namespace configuration migration",id:"namespace-configuration-migration",level:2},{value:"Proxy disconnect code changes",id:"proxy-disconnect-code-changes",level:2},{value:"Other configuration option changes",id:"other-configuration-option-changes",level:2},{value:"Server API changes",id:"server-api-changes",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",strong:"strong",ul:"ul",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"Centrifugo v4 development was concentrated around two main things:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"adopt a new generation of client protocol"}),"\n",(0,i.jsx)(n.li,{children:"make namespaces secure by default"}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"These goals dictate most of backwards compatibility changes in v4."}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"What we would like to emphasize is that even there are many backwards incompatible changes it should be possible to migrate to Centrifugo v4 server without changing your client-side code at all. And then gradually upgrade the client-side. Below we are giving all the tips to achieve this."})}),"\n",(0,i.jsx)(n.h2,{id:"client-sdk-migration",children:"Client SDK migration"}),"\n",(0,i.jsx)(n.p,{children:"New generation of client protocol requires using the latest versions of client SDKs. During the next several days we will release the following SDK versions which are compatible with Centrifugo v4:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"centrifuge-js >= v3.0.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-go >= v0.9.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-dart >= v0.9.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-swift >= v0.5.0"}),"\n",(0,i.jsx)(n.li,{children:"centrifuge-java >= v0.2.0"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["New client SDKs ",(0,i.jsx)(n.strong,{children:"support only new client protocol"})," \u2013 you can not connect to Centrifugo v3 with them."]}),"\n",(0,i.jsx)(n.p,{children:"If you have a production system where you want to upgrade Centrifugo from v3 to v4 then the plan is:"}),"\n",(0,i.jsx)(n.admonition,{type:"danger",children:(0,i.jsxs)(n.p,{children:["If you are using private channels (starting with ",(0,i.jsx)(n.code,{children:"$"}),") or user-limited channels (containing ",(0,i.jsx)(n.code,{children:"#"}),") then carefully read about subscription token migration and user-limited channels migration below."]})}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsx)(n.li,{children:"Upgrade Centrifugo and its configuration to adopt changes in v4."}),"\n",(0,i.jsxs)(n.li,{children:["In Centrifugo v4 config turn on ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Run Centrifugo v4 \u2013 all current clients should continue working with it."}),"\n",(0,i.jsxs)(n.li,{children:["Then on the client-side uprade client SDK version to the one which works with Centrifugo v4, adopt changes in SDK API dictated by our new ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api",children:"client SDK API spec"}),". ",(0,i.jsx)(n.strong,{children:"Important thing"})," \u2013 add ",(0,i.jsx)(n.code,{children:"?cf_protocol_version=v2"})," URL param to the connection endpoint to tell Centrifugo that modern generation of protocol is being used by the connection (otherwise, it assumes old protocol since we have ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option enabled)."]}),"\n",(0,i.jsxs)(n.li,{children:["As soon as all your clients migrated to use new protocol generation you can remove ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option from the server configuration."]}),"\n",(0,i.jsxs)(n.li,{children:["After that you can remove ",(0,i.jsx)(n.code,{children:"?cf_protocol_version=v2"})," from connection endpoint on the client-side."]}),"\n"]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"If you are using mobile client SDKs then most probably some time must pass while clients update their apps to use an updated Centrifugo SDK version."})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Starting from Centrifugo v4.1.1 it's possible to completely turn off client protocol v1 by setting ",(0,i.jsx)(n.code,{children:"disable_client_protocol_v1"})," boolean option to ",(0,i.jsx)(n.code,{children:"true"}),"."]})}),"\n",(0,i.jsx)(n.h2,{id:"unidirectional-transport-migration",children:"Unidirectional transport migration"}),"\n",(0,i.jsx)(n.p,{children:"Client protocol framing also changed in unidirectional transports. The good news is that Centrifugo v4 still supports previous format for unidirectional transports."}),"\n",(0,i.jsxs)(n.p,{children:["When you are enabling ",(0,i.jsx)(n.code,{children:"use_client_protocol_v1_by_default"})," option described above you also make unidirectional transports to work over old protocol format. So your existing clients will continue working just fine with Centrifugo v4. Then the same steps to migrate described above can be applied to unidirectional transport case. The only difference that in unidirectional approach you are not using Centrifugo SDKs."]}),"\n",(0,i.jsx)(n.h2,{id:"sockjs-migration",children:"SockJS migration"}),"\n",(0,i.jsx)(n.p,{children:"SockJS is now DEPRECATED in Centrifugo. Centrifugo v4 may be the last release which supports it. We now offer our own bidirectional emulation layer on top of HTTP-streaming and EventSource. See additional information in Centrifugo v4 introduction post."}),"\n",(0,i.jsx)(n.h2,{id:"channel-ascii-enforced",children:"Channel ASCII enforced"}),"\n",(0,i.jsxs)(n.p,{children:["Centrifugo v2 and v3 docs mentioned the fact that channels must contain only ASCII characters. But it was not actually enforced by a server. Now Centrifugo is more strict. If a channel has non-ASCII characters then the ",(0,i.jsx)(n.code,{children:"107: bad request"})," error will be returned to the client. Please reach us out if this behavior is not suitable for your use case \u2013 we can discuss the use case and think on a proper solution together."]}),"\n",(0,i.jsx)(n.h2,{id:"subscription-token-migration",children:"Subscription token migration"}),"\n",(0,i.jsxs)(n.p,{children:["Subscription token now requires ",(0,i.jsx)(n.code,{children:"sub"})," claim (current user ID) to be set."]}),"\n",(0,i.jsxs)(n.p,{children:["In most cases the only change which is required to smoothly migrate to v4 without breaking things is to add a boolean option ",(0,i.jsx)(n.code,{children:'"skip_user_check_in_subscription_token": true'})," to a Centrifugo v4 configuration. This skips the check of ",(0,i.jsx)(n.code,{children:"sub"})," claim to contain the current user ID set to a connection during authentication."]}),"\n",(0,i.jsxs)(n.p,{children:["After that start adding ",(0,i.jsx)(n.code,{children:"sub"})," claim (with current user ID) to subscription tokens. As soon as all subscription tokens in your system contain user ID in ",(0,i.jsx)(n.code,{children:"sub"})," claim you can remove the ",(0,i.jsx)(n.code,{children:"skip_user_check_in_subscription_token"})," from a server configuration."]}),"\n",(0,i.jsxs)(n.p,{children:["One more important note is that ",(0,i.jsx)(n.code,{children:"client"})," claim in subscription token in Centrifugo v4 only supported for backwards compatibility. It must not be included into new subscription tokens."]}),"\n",(0,i.jsxs)(n.p,{children:["It's worth mentioning that Centrifugo v4 does not allow subscribing on channels starting with ",(0,i.jsx)(n.code,{children:"$"})," without token even if namespace marked as available for subscribing using sth like ",(0,i.jsx)(n.code,{children:"allow_subscribe_for_client"})," option. This is done to prevent potential security risk during v3 -> v4 migration when client previously not available to subscribe to channels starting with ",(0,i.jsx)(n.code,{children:"$"})," in any case may get permissions to do so."]}),"\n",(0,i.jsx)(n.h2,{id:"user-limited-channel-migration",children:"User-limited channel migration"}),"\n",(0,i.jsxs)(n.p,{children:["User-limited channel support should now be allowed over a separate channel namespace option ",(0,i.jsx)(n.code,{children:"allow_user_limited_channels"}),". See below the namespace option converter which takes this change into account."]}),"\n",(0,i.jsx)(n.h2,{id:"namespace-configuration-migration",children:"Namespace configuration migration"}),"\n",(0,i.jsxs)(n.p,{children:["In Centrifugo v4 namespace configuration options have been changed. Centrifugo now has ",(0,i.jsx)(n.code,{children:"secure by default"})," namespaces. First thing to do is to read the new docs about ",(0,i.jsx)(n.a,{href:"/docs/server/channels",children:"channels and namespaces"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Then you can use the following converter which will transform your old namespace configuration to a new one. This converter tries to keep backwards compatibility \u2013 i.e. it should be possible to deploy Centrifugo with namespace configuration from converter output and have the same behaviour as before regarding channel permissions. We believe that new option names should provide a more readable configuration and may help to reveal some potential security improvements in your namespace configuration \u2013 i.e. making it more strict and protective."}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsx)(n.p,{children:"Do not blindly deploy things to production \u2013 test your system first, go through the possible usage scenarios and/or test cases."})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsx)(n.p,{children:"It's fully client-side: your data won't be sent anywhere."})}),"\n","\n",(0,i.jsx)(r.Z,{}),"\n",(0,i.jsx)(n.h2,{id:"proxy-disconnect-code-changes",children:"Proxy disconnect code changes"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"reconnect"})," flag from custom disconnect code is removed. Reconnect advice is now determined by disconnect code value. This allowed us avoiding using JSON in WebSocket CLOSE frame reason. See ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#return-custom-disconnect",children:"proxy docs"})," docs for more details."]}),"\n",(0,i.jsx)(n.h2,{id:"other-configuration-option-changes",children:"Other configuration option changes"}),"\n",(0,i.jsx)(n.p,{children:"Several other non-namespace related options have been renamed or removed:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"client_anonymous"})," option renamed to ",(0,i.jsx)(n.code,{children:"allow_anonymous_connect_without_token"})," \u2013 new name better describes the purpose of this option which was previously not clear. Converter above takes this into account."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"use_unlimited_history_by_default"})," option was removed. It was used to help migrating from Centrifugo v2 to v3."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"server-api-changes",children:"Server API changes"}),"\n",(0,i.jsxs)(n.p,{children:["The only breaking change is that ",(0,i.jsx)(n.code,{children:"user_connections"})," API method (which is available in Centrifugo PRO only) was renamed to ",(0,i.jsx)(n.code,{children:"connections"}),". The method is more generic now with a broader possibilities \u2013 so previous name does not match the current behavior."]})]})}function u(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},5717:(e,n,o)=>{o.d(n,{Z:()=>r});var i=o(67294),t=o(85893);class r extends i.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={config:null,output:"Here will be configuration for v4",logs:"Here will be log of changes made in your config"}}onClick(e){if(!this.state.config)return void alert("Provide a configuration");let n;try{n=JSON.parse(this.state.config)}catch{return void alert("Invalid JSON")}let o=[],i=[],t=function(e){let n="config top-level";return void 0!==e&&(n="namespace {"+e.name+"}"),n},r=function(e,r,s){i.push("`"+e+"` renamed to `"+r+"`");let a=t(s);void 0===s&&(s=n),void 0===s[r]&&void 0!==s[e]&&(s[r]=s[e],delete s[e],o.push("renamed "+e+" to "+r+" in "+a))},s=function(e,r){i.push("`"+e+"` removed");let s=t(r);void 0===r&&(r=n),void 0!==r[e]&&(delete r[e],o.push("removed "+e+" from "+s))},a=function(e,r,s){i.push("`"+e+"` is now required");let a=t(s);void 0===s&&(s=n),void 0===s[e]&&(s[e]=r,o.push("added "+e+" to "+a))};s("use_unlimited_history_by_default"),r("client_anonymous","allow_anonymous_connect_without_token");let c=n;if(a("allow_user_limited_channels",!0),!0===c.protected?s("protected"):(a("allow_subscribe_for_client",!0),r("anonymous","allow_subscribe_for_anonymous")),!0===c.publish&&(r("publish","allow_publish_for_client"),a("allow_publish_for_anonymous",!0)),!0===c.presence&&(!0===c.presence_disabled_for_client?s("presence_disabled_for_client"):(a("allow_presence_for_subscriber",!0),a("allow_presence_for_anonymous",!0))),void 0!==c.history_ttl&&void 0!==c.history_size&&(!0===c.history_disabled_for_client?s("history_disabled_for_client"):(a("allow_history_for_subscriber",!0),a("allow_history_for_anonymous",!0))),!0===c.position?r("position","force_positioning"):s("position"),!0===c.recover?r("recover","force_recovery"):s("recover"),!0===c.join_leave&&a("force_push_join_leave",!0),void 0!==n.namespaces){let e=[];for(let o of n.namespaces)a("allow_user_limited_channels",!0,o),!0===o.protected?s("protected",o):(a("allow_subscribe_for_client",!0,o),r("anonymous","allow_subscribe_for_anonymous",o)),!0===o.publish&&(r("publish","allow_publish_for_client",o),a("allow_publish_for_anonymous",!0,o)),!0===o.presence&&(!0===o.presence_disabled_for_client?s("presence_disabled_for_client",o):(a("allow_presence_for_subscriber",!0,o),a("allow_presence_for_anonymous",!0,o))),void 0!==o.history_ttl&&void 0!==o.history_size&&(!0===o.history_disabled_for_client?s("history_disabled_for_client",o):(a("allow_history_for_subscriber",!0,o),a("allow_history_for_anonymous",!0,o))),!0===o.position?r("position","force_positioning",o):s("position",o),!0===o.recover?r("recover","force_recovery",o):s("recover",o),!0===o.join_leave&&a("force_push_join_leave",!0),e.push(o);n.namespaces=e}this.setState({output:JSON.stringify(n,null,"\t")}),this.setState({logs:JSON.stringify(o,null,"\t")}),console.log(i.join("\n\n"))}onChange(e){this.setState({config:e.target.value})}render(){return(0,t.jsxs)("div",{children:[(0,t.jsx)("textarea",{onChange:this.onChange,placeholder:"Paste your Centrifugo v3 JSON config here...",style:{width:"100%",height:"300px",border:"2px solid #ccc"}}),(0,t.jsx)("button",{onClick:this.onClick,children:"Convert"}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.output}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.logs})]})}}},11151:(e,n,o)=>{o.d(n,{Z:()=>a,a:()=>s});var i=o(67294);const t={},r=i.createContext(t);function s(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2b147458.7064fa13.js b/assets/js/2b147458.a382d9a1.js similarity index 59% rename from assets/js/2b147458.7064fa13.js rename to assets/js/2b147458.a382d9a1.js index 724d85c5c..f03c6ed2d 100644 --- a/assets/js/2b147458.7064fa13.js +++ b/assets/js/2b147458.a382d9a1.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2757],{42859:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>r,metadata:()=>d,toc:()=>c});var s=i(85893),t=i(11151);const r={id:"admin_web",title:"Admin web UI"},o=void 0,d={id:"server/admin_web",title:"Admin web UI",description:"Centrifugo comes with a built-in administrative web interface. It enables users to:",source:"@site/docs/server/admin_web.md",sourceDirName:"server",slug:"/server/admin_web",permalink:"/docs/server/admin_web",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/admin_web.md",tags:[],version:"current",frontMatter:{id:"admin_web",title:"Admin web UI"},sidebar:"Guides",previous:{title:"Proxy subscription streams",permalink:"/docs/server/proxy_streams"},next:{title:"Server observability",permalink:"/docs/server/observability"}},a={},c=[{value:"Options",id:"options",level:2},{value:"Using custom web interface",id:"using-custom-web-interface",level:2},{value:"Admin insecure mode",id:"admin-insecure-mode",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Centrifugo comes with a built-in administrative web interface. It enables users to:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Display general information and statistics from server nodes - including the number of connections, unique users, subscriptions, and unique channels, among others"}),"\n",(0,s.jsxs)(n.li,{children:["Execute commands such as ",(0,s.jsx)(n.code,{children:"publish"}),", ",(0,s.jsx)(n.code,{children:"broadcast"}),", ",(0,s.jsx)(n.code,{children:"subscribe"}),", ",(0,s.jsx)(n.code,{children:"unsubscribe"}),", ",(0,s.jsx)(n.code,{children:"disconnect"}),", ",(0,s.jsx)(n.code,{children:"history"}),", ",(0,s.jsx)(n.code,{children:"history_remove"}),", ",(0,s.jsx)(n.code,{children:"presence"}),", ",(0,s.jsx)(n.code,{children:"presence_stats"}),", ",(0,s.jsx)(n.code,{children:"info"}),", ",(0,s.jsx)(n.code,{children:"channels"}),", along with several additional Centrifugo PRO server API commands."]}),"\n",(0,s.jsx)(n.li,{children:"Trace connections in real-time (a feature Centrifugo PRO)."}),"\n",(0,s.jsx)(n.li,{children:"View analytics widgets (a feature of Centrifugo PRO)."}),"\n",(0,s.jsx)(n.li,{children:"Visualize registered devices for push notifications (a feature of Centrifugo PRO)."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["To activate the administrative web interface, run Centrifugo with the ",(0,s.jsx)(n.code,{children:"admin"})," option enabled and configure security settings in the configuration file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "admin": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>"\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"options",children:"Options"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin"})," (boolean, default: ",(0,s.jsx)(n.code,{children:"false"}),") \u2013 enables/disables admin web UI"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin_password"})," (string, default: ",(0,s.jsx)(n.code,{children:'""'}),") \u2013 this is a password to log into admin web interface"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin_secret"})," (string, default: ",(0,s.jsx)(n.code,{children:'""'}),") - this is a secret key for authentication token set on successful login."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Make both ",(0,s.jsx)(n.code,{children:"admin_password"})," and ",(0,s.jsx)(n.code,{children:"admin_secret"})," strong and keep them in secret."]}),"\n",(0,s.jsxs)(n.p,{children:["After configuring, restart Centrifugo and go to ",(0,s.jsx)(n.a,{href:"http://localhost:8000",children:"http://localhost:8000"})," (by default) - you should see web interface."]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Although there is a password based authentication a good advice is to protect web interface by firewall rules in production."})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.img,{alt:"Admin web panel",src:i(45593).Z+"",width:"2378",height:"1304"}),"\nLog in using ",(0,s.jsx)(n.code,{children:"admin_password"})," value:\n",(0,s.jsx)(n.img,{alt:"Admin web panel",src:i(21907).Z+"",width:"2528",height:"1246"})]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["Centrifugo PRO ",(0,s.jsx)(n.a,{href:"/docs/pro/admin_idp_auth",children:"supports Single Sign-On"})," (SSO) authentication for web interface using OpenID Connect (OIDC) protocol."]})}),"\n",(0,s.jsx)(n.h2,{id:"using-custom-web-interface",children:"Using custom web interface"}),"\n",(0,s.jsx)(n.p,{children:"If you want to use custom web interface you can specify path to web interface directory dist:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...,\n "admin": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>",\n "admin_web_path": "<PATH_TO_WEB_DIST>"\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This can be useful if you want to modify official ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/web",children:"web interface code"})," in some way and test it with Centrifugo."]}),"\n",(0,s.jsx)(n.h2,{id:"admin-insecure-mode",children:"Admin insecure mode"}),"\n",(0,s.jsxs)(n.p,{children:["There is also an option to run Centrifugo in insecure admin mode. In this mode, it's unnecessary to set ",(0,s.jsx)(n.code,{children:"admin_password"})," and ",(0,s.jsx)(n.code,{children:"admin_secret"})," in the configuration \u2013 you will be automatically logged into the web interface without any password. Note that this mode should only be considered for production if you have protected the admin web interface with firewall rules. Without such protection, anyone on the internet would have full access to the admin functionalities described above. To enable insecure admin mode:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...,\n "admin": true,\n "admin_insecure": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>"\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},21907:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/admin_three_nodes-7cde31543f1ae091cd8de008ea98f245.png"},45593:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/quick_start_admin_v5-52b31f2dfd00de4c5169e2b24c71bd1f.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>d,a:()=>o});var s=i(67294);const t={},r=s.createContext(t);function o(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2757],{42859:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>r,metadata:()=>d,toc:()=>c});var s=i(85893),t=i(11151);const r={id:"admin_web",title:"Admin web UI"},o=void 0,d={id:"server/admin_web",title:"Admin web UI",description:"Centrifugo comes with a built-in administrative web interface. It enables users to:",source:"@site/docs/server/admin_web.md",sourceDirName:"server",slug:"/server/admin_web",permalink:"/docs/server/admin_web",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/admin_web.md",tags:[],version:"current",frontMatter:{id:"admin_web",title:"Admin web UI"},sidebar:"Guides",previous:{title:"Proxy subscription streams",permalink:"/docs/server/proxy_streams"},next:{title:"Server observability",permalink:"/docs/server/observability"}},a={},c=[{value:"Options",id:"options",level:2},{value:"Using custom web interface",id:"using-custom-web-interface",level:2},{value:"Admin insecure mode",id:"admin-insecure-mode",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Centrifugo comes with a built-in administrative web interface. It enables users to:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Display general information and statistics from server nodes - including the number of connections, unique users, subscriptions, and unique channels, among others"}),"\n",(0,s.jsxs)(n.li,{children:["Execute commands such as ",(0,s.jsx)(n.code,{children:"publish"}),", ",(0,s.jsx)(n.code,{children:"broadcast"}),", ",(0,s.jsx)(n.code,{children:"subscribe"}),", ",(0,s.jsx)(n.code,{children:"unsubscribe"}),", ",(0,s.jsx)(n.code,{children:"disconnect"}),", ",(0,s.jsx)(n.code,{children:"history"}),", ",(0,s.jsx)(n.code,{children:"history_remove"}),", ",(0,s.jsx)(n.code,{children:"presence"}),", ",(0,s.jsx)(n.code,{children:"presence_stats"}),", ",(0,s.jsx)(n.code,{children:"info"}),", ",(0,s.jsx)(n.code,{children:"channels"}),", along with several additional Centrifugo PRO server API commands."]}),"\n",(0,s.jsx)(n.li,{children:"Trace connections in real-time (a feature Centrifugo PRO)."}),"\n",(0,s.jsx)(n.li,{children:"View analytics widgets (a feature of Centrifugo PRO)."}),"\n",(0,s.jsx)(n.li,{children:"Visualize registered devices for push notifications (a feature of Centrifugo PRO)."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["To activate the administrative web interface, run Centrifugo with the ",(0,s.jsx)(n.code,{children:"admin"})," option enabled and configure security settings in the configuration file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "admin": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>"\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"options",children:"Options"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin"})," (boolean, default: ",(0,s.jsx)(n.code,{children:"false"}),") \u2013 enables/disables admin web UI"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin_password"})," (string, default: ",(0,s.jsx)(n.code,{children:'""'}),") \u2013 this is a password to log into admin web interface"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"admin_secret"})," (string, default: ",(0,s.jsx)(n.code,{children:'""'}),") - this is a secret key for authentication token set on successful login."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Make both ",(0,s.jsx)(n.code,{children:"admin_password"})," and ",(0,s.jsx)(n.code,{children:"admin_secret"})," strong and keep them in secret."]}),"\n",(0,s.jsxs)(n.p,{children:["After configuring, restart Centrifugo and go to ",(0,s.jsx)(n.a,{href:"http://localhost:8000",children:"http://localhost:8000"})," (by default) - you should see web interface."]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"Although there is a password based authentication a good advice is to protect web interface by firewall rules in production."})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.img,{alt:"Admin web panel",src:i(5218).Z+"",width:"2378",height:"1304"}),"\nLog in using ",(0,s.jsx)(n.code,{children:"admin_password"})," value:\n",(0,s.jsx)(n.img,{alt:"Admin web panel",src:i(89558).Z+"",width:"2528",height:"1246"})]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["Centrifugo PRO ",(0,s.jsx)(n.a,{href:"/docs/pro/admin_idp_auth",children:"supports Single Sign-On"})," (SSO) authentication for web interface using OpenID Connect (OIDC) protocol."]})}),"\n",(0,s.jsx)(n.h2,{id:"using-custom-web-interface",children:"Using custom web interface"}),"\n",(0,s.jsx)(n.p,{children:"If you want to use custom web interface you can specify path to web interface directory dist:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...,\n "admin": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>",\n "admin_web_path": "<PATH_TO_WEB_DIST>"\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["This can be useful if you want to modify official ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/web",children:"web interface code"})," in some way and test it with Centrifugo."]}),"\n",(0,s.jsx)(n.h2,{id:"admin-insecure-mode",children:"Admin insecure mode"}),"\n",(0,s.jsxs)(n.p,{children:["There is also an option to run Centrifugo in insecure admin mode. In this mode, it's unnecessary to set ",(0,s.jsx)(n.code,{children:"admin_password"})," and ",(0,s.jsx)(n.code,{children:"admin_secret"})," in the configuration \u2013 you will be automatically logged into the web interface without any password. Note that this mode should only be considered for production if you have protected the admin web interface with firewall rules. Without such protection, anyone on the internet would have full access to the admin functionalities described above. To enable insecure admin mode:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...,\n "admin": true,\n "admin_insecure": true,\n "admin_password": "<PASSWORD>",\n "admin_secret": "<SECRET>"\n}\n'})})]})}function u(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},89558:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/admin_three_nodes-7cde31543f1ae091cd8de008ea98f245.png"},5218:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/quick_start_admin_v5-52b31f2dfd00de4c5169e2b24c71bd1f.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>d,a:()=>o});var s=i(67294);const t={},r=s.createContext(t);function o(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2dbf7ee0.980ae824.js b/assets/js/2dbf7ee0.6722c7f6.js similarity index 99% rename from assets/js/2dbf7ee0.980ae824.js rename to assets/js/2dbf7ee0.6722c7f6.js index a28a67b5b..86cd79cd3 100644 --- a/assets/js/2dbf7ee0.980ae824.js +++ b/assets/js/2dbf7ee0.6722c7f6.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3581],{86064:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>p,frontMatter:()=>t,metadata:()=>o,toc:()=>l});var i=s(85893),r=s(11151);const t={id:"presence",title:"Online presence"},a=void 0,o={id:"server/presence",title:"Online presence",description:"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.",source:"@site/docs/server/presence.md",sourceDirName:"server",slug:"/server/presence",permalink:"/docs/server/presence",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/presence.md",tags:[],version:"current",frontMatter:{id:"presence",title:"Online presence"},sidebar:"Guides",previous:{title:"Delta compression",permalink:"/docs/server/delta_compression"},next:{title:"Proxy events to the backend",permalink:"/docs/server/proxy"}},c={},l=[{value:"Enabling online presence",id:"enabling-online-presence",level:2},{value:"Retrieving presence on the client side",id:"retrieving-presence-on-the-client-side",level:2},{value:"Join and leave events",id:"join-and-leave-events",level:2},{value:"Implementation notes",id:"implementation-notes",level:2},{value:"Conclusion",id:"conclusion",level:2}];function d(e){const n={a:"a",code:"code",h2:"h2",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"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."}),"\n",(0,i.jsx)(n.h2,{id:"enabling-online-presence",children:"Enabling online presence"}),"\n",(0,i.jsxs)(n.p,{children:["To enable online presence, you need to set the ",(0,i.jsx)(n.code,{children:"presence"})," option to ",(0,i.jsx)(n.code,{children:"true"})," for the specific channel namespace in your Centrifugo configuration."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [{\n "name": "public",\n "presence": true\n }]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"After enabling this you can query presence information over server HTTP/GRPC presence call:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: YOUR_API_KEY" \\\n --request POST \\\n --data \'{"channel": "public:test"}\' \\\n http://localhost:8000/api/presence\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence",children:"description of presence API"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Also, a shorter version of presence which only contains two counters - number of clients and number of unique users in channel - may be called:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: YOUR_API_KEY" \\\n --request POST \\\n --data \'{"channel": "public:test"}\' \\\n http://localhost:8000/api/presence_stats\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence_stats",children:"description of presence stats API"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"retrieving-presence-on-the-client-side",children:"Retrieving presence on the client side"}),"\n",(0,i.jsx)(n.p,{children:"Once presence enabled, you can retrieve the presence information on the client side too by calling the presence method on the channel."}),"\n",(0,i.jsxs)(n.p,{children:["To do this you need to ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#presence-permission-model",children:"give the client permission to call presence"}),". Once done, presence may be retrieved from the subscription:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presence(channel);\n"})}),"\n",(0,i.jsx)(n.p,{children:"It's also available on the top-level of the client (for example, if you need to call presence for server-side subscription):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await client.presence(channel);\n"})}),"\n",(0,i.jsx)(n.p,{children:"If the permission check has passed successfully \u2013 both methods will return an object containing information about currently subscribed clients."}),"\n",(0,i.jsxs)(n.p,{children:["Also, ",(0,i.jsx)(n.code,{children:"presenceStats"})," method is avalable:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presenceStats(channel);\n"})}),"\n",(0,i.jsx)(n.h2,{id:"join-and-leave-events",children:"Join and leave events"}),"\n",(0,i.jsxs)(n.p,{children:["It's also possible to enable real-time tracking of users joining or leaving a channel by listening to ",(0,i.jsx)(n.code,{children:"join"})," and ",(0,i.jsx)(n.code,{children:"leave"})," events on the client side."]}),"\n",(0,i.jsx)(n.p,{children:"By default, Centrifugo does not send these events and they must be explicitly turned on for channel namespace:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [{\n "name": "public",\n "presence": true,\n "join_leave": true,\n "force_push_join_leave": true\n }]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Then on the client side:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"subscription.on('join', function(joinCtx) {\n console.log('client joined:', joinCtx);\n});\n\nsubscription.on('leave', function(leaveCtx) {\n console.log('client left:', leaveCtx);\n});\n"})}),"\n",(0,i.jsxs)(n.p,{children:["And the same on ",(0,i.jsx)(n.code,{children:"client"})," top-level for the needs of server-side subscriptions (analogous to the presence call described above)."]}),"\n",(0,i.jsx)(n.p,{children:"These events provide real-time updates and can be used to keep track of user activity and manage live interactions."}),"\n",(0,i.jsx)(n.p,{children:"You can combine join/leave events with presence information and maintain a list of currently active subscribers - for example show the list of online players in the game room updated in real-time."}),"\n",(0,i.jsx)(n.h2,{id:"implementation-notes",children:"Implementation notes"}),"\n",(0,i.jsx)(n.p,{children:"The online presence feature might increase the load on your Centrifugo server, since Centrifugo need to maintain an addition data structure. Therefore, it is recommended to use this feature judiciously based on your server's capability and the necessity of real-time presence data in your application."}),"\n",(0,i.jsx)(n.p,{children:"Always make sure to secure the presence data, as it could expose sensitive information about user activity in your application. Ensure appropriate security measures are in place."}),"\n",(0,i.jsx)(n.p,{children:"Join and leave events delivered with at most once guarantee."}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#online-presence-considerations",children:"more about presence design"})," in design overview chapter."]}),"\n",(0,i.jsxs)(n.p,{children:["Also ",(0,i.jsx)(n.a,{href:"/docs/faq/#how-scalable-is-the-online-presence-and-joinleave-features",children:"check out FAQ"})," which mentions scalability concerns for presence data and join/leave events."]}),"\n",(0,i.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(n.p,{children:"The online presence feature of Centrifugo is a highly useful tool for real-time applications. It provides instant and live data about user activity, which can be critical for interactive features in chats, collaborative tools, multiplayer games, or live tracking systems. Make sure to configure and use this feature appropriately to get the most out of your real-time application."})]})}function p(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},11151:(e,n,s)=>{s.d(n,{Z:()=>o,a:()=>a});var i=s(67294);const r={},t=i.createContext(r);function a(e){const n=i.useContext(t);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3581],{12057:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>p,frontMatter:()=>t,metadata:()=>o,toc:()=>l});var i=s(85893),r=s(11151);const t={id:"presence",title:"Online presence"},a=void 0,o={id:"server/presence",title:"Online presence",description:"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.",source:"@site/docs/server/presence.md",sourceDirName:"server",slug:"/server/presence",permalink:"/docs/server/presence",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/presence.md",tags:[],version:"current",frontMatter:{id:"presence",title:"Online presence"},sidebar:"Guides",previous:{title:"Delta compression",permalink:"/docs/server/delta_compression"},next:{title:"Proxy events to the backend",permalink:"/docs/server/proxy"}},c={},l=[{value:"Enabling online presence",id:"enabling-online-presence",level:2},{value:"Retrieving presence on the client side",id:"retrieving-presence-on-the-client-side",level:2},{value:"Join and leave events",id:"join-and-leave-events",level:2},{value:"Implementation notes",id:"implementation-notes",level:2},{value:"Conclusion",id:"conclusion",level:2}];function d(e){const n={a:"a",code:"code",h2:"h2",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"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."}),"\n",(0,i.jsx)(n.h2,{id:"enabling-online-presence",children:"Enabling online presence"}),"\n",(0,i.jsxs)(n.p,{children:["To enable online presence, you need to set the ",(0,i.jsx)(n.code,{children:"presence"})," option to ",(0,i.jsx)(n.code,{children:"true"})," for the specific channel namespace in your Centrifugo configuration."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [{\n "name": "public",\n "presence": true\n }]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"After enabling this you can query presence information over server HTTP/GRPC presence call:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: YOUR_API_KEY" \\\n --request POST \\\n --data \'{"channel": "public:test"}\' \\\n http://localhost:8000/api/presence\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence",children:"description of presence API"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Also, a shorter version of presence which only contains two counters - number of clients and number of unique users in channel - may be called:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:'curl --header "Content-Type: application/json" \\\n --header "X-API-Key: YOUR_API_KEY" \\\n --request POST \\\n --data \'{"channel": "public:test"}\' \\\n http://localhost:8000/api/presence_stats\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/server/server_api#presence_stats",children:"description of presence stats API"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"retrieving-presence-on-the-client-side",children:"Retrieving presence on the client side"}),"\n",(0,i.jsx)(n.p,{children:"Once presence enabled, you can retrieve the presence information on the client side too by calling the presence method on the channel."}),"\n",(0,i.jsxs)(n.p,{children:["To do this you need to ",(0,i.jsx)(n.a,{href:"/docs/server/channel_permissions#presence-permission-model",children:"give the client permission to call presence"}),". Once done, presence may be retrieved from the subscription:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presence(channel);\n"})}),"\n",(0,i.jsx)(n.p,{children:"It's also available on the top-level of the client (for example, if you need to call presence for server-side subscription):"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await client.presence(channel);\n"})}),"\n",(0,i.jsx)(n.p,{children:"If the permission check has passed successfully \u2013 both methods will return an object containing information about currently subscribed clients."}),"\n",(0,i.jsxs)(n.p,{children:["Also, ",(0,i.jsx)(n.code,{children:"presenceStats"})," method is avalable:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presenceStats(channel);\n"})}),"\n",(0,i.jsx)(n.h2,{id:"join-and-leave-events",children:"Join and leave events"}),"\n",(0,i.jsxs)(n.p,{children:["It's also possible to enable real-time tracking of users joining or leaving a channel by listening to ",(0,i.jsx)(n.code,{children:"join"})," and ",(0,i.jsx)(n.code,{children:"leave"})," events on the client side."]}),"\n",(0,i.jsx)(n.p,{children:"By default, Centrifugo does not send these events and they must be explicitly turned on for channel namespace:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [{\n "name": "public",\n "presence": true,\n "join_leave": true,\n "force_push_join_leave": true\n }]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Then on the client side:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"subscription.on('join', function(joinCtx) {\n console.log('client joined:', joinCtx);\n});\n\nsubscription.on('leave', function(leaveCtx) {\n console.log('client left:', leaveCtx);\n});\n"})}),"\n",(0,i.jsxs)(n.p,{children:["And the same on ",(0,i.jsx)(n.code,{children:"client"})," top-level for the needs of server-side subscriptions (analogous to the presence call described above)."]}),"\n",(0,i.jsx)(n.p,{children:"These events provide real-time updates and can be used to keep track of user activity and manage live interactions."}),"\n",(0,i.jsx)(n.p,{children:"You can combine join/leave events with presence information and maintain a list of currently active subscribers - for example show the list of online players in the game room updated in real-time."}),"\n",(0,i.jsx)(n.h2,{id:"implementation-notes",children:"Implementation notes"}),"\n",(0,i.jsx)(n.p,{children:"The online presence feature might increase the load on your Centrifugo server, since Centrifugo need to maintain an addition data structure. Therefore, it is recommended to use this feature judiciously based on your server's capability and the necessity of real-time presence data in your application."}),"\n",(0,i.jsx)(n.p,{children:"Always make sure to secure the presence data, as it could expose sensitive information about user activity in your application. Ensure appropriate security measures are in place."}),"\n",(0,i.jsx)(n.p,{children:"Join and leave events delivered with at most once guarantee."}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/getting-started/design#online-presence-considerations",children:"more about presence design"})," in design overview chapter."]}),"\n",(0,i.jsxs)(n.p,{children:["Also ",(0,i.jsx)(n.a,{href:"/docs/faq/#how-scalable-is-the-online-presence-and-joinleave-features",children:"check out FAQ"})," which mentions scalability concerns for presence data and join/leave events."]}),"\n",(0,i.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,i.jsx)(n.p,{children:"The online presence feature of Centrifugo is a highly useful tool for real-time applications. It provides instant and live data about user activity, which can be critical for interactive features in chats, collaborative tools, multiplayer games, or live tracking systems. Make sure to configure and use this feature appropriately to get the most out of your real-time application."})]})}function p(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},11151:(e,n,s)=>{s.d(n,{Z:()=>o,a:()=>a});var i=s(67294);const r={},t=i.createContext(r);function a(e){const n=i.useContext(t);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2e854b47.fb6d8c31.js b/assets/js/2e854b47.3bd3e607.js similarity index 98% rename from assets/js/2e854b47.fb6d8c31.js rename to assets/js/2e854b47.3bd3e607.js index ae8e4735f..663f2de41 100644 --- a/assets/js/2e854b47.fb6d8c31.js +++ b/assets/js/2e854b47.3bd3e607.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7287],{69608:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>s,toc:()=>l});var o=n(85893),a=n(11151);const i={id:"intro",sidebar_label:"Real-time app from scratch",title:"Building WebSocket chat (messenger) app from scratch"},r=void 0,s={id:"tutorial/intro",title:"Building WebSocket chat (messenger) app from scratch",description:"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.",source:"@site/docs/tutorial/intro.md",sourceDirName:"tutorial",slug:"/tutorial/intro",permalink:"/docs/tutorial/intro",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/intro.md",tags:[],version:"current",frontMatter:{id:"intro",sidebar_label:"Real-time app from scratch",title:"Building WebSocket chat (messenger) app from scratch"},sidebar:"Tutorial",next:{title:"App layout and behavior",permalink:"/docs/tutorial/layout"}},c={},l=[{value:"Application tech stack",id:"application-tech-stack",level:2},{value:"Straight to the source code",id:"straight-to-the-source-code",level:2},{value:"Centrifugo vs Django Channels",id:"centrifugo-vs-django-channels",level:2}];function h(e){const t={a:"a",h2:"h2",img:"img",p:"p",strong:"strong",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"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."}),"\n",(0,o.jsxs)(t.p,{children:["The app we build here is a WebSocket chat called ",(0,o.jsx)(t.strong,{children:"GrandChat"}),". The internet is full of chat tutorials, but we promise \u2013 here, we go beyond the usual basics. GrandChat is not just a set of isolated chat rooms but more like a messenger application, a simplified version of Discord, Telegram, or Slack. Here is a short demo of our final result:"]}),"\n",(0,o.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:!0,src:"/img/grand-chat-tutorial-demo.mp4"}),"\n",(0,o.jsx)(t.p,{children:"Note that we have a real-time synchronization across the app \u2013 room membership events and room messages are sent in real-time. Our design allows users to be subscribed to many rooms and receive updates from all of them within one screen. To achieve this in a scalable way we use individual channel for each application user. We will show how the app scales when there are thousands of room members to prove that with almost no additional effort it may scale to the size comparable to the largest Slack messenger installations with reasonable latency properties."}),"\n",(0,o.jsx)(t.h2,{id:"application-tech-stack",children:"Application tech stack"}),"\n",(0,o.jsx)(t.p,{children:"Centrifugo is completely agnostic to the technology stack, seamlessly integrating with any frontend or backend technologies. However, for the purpose of this tutorial, we needed to choose specific technologies to illustrate the entire process of building a real-time WebSocket app:"}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e On the frontend, we utilize ",(0,o.jsx)(t.a,{href:"https://react.dev/",children:"React"})," and ",(0,o.jsx)(t.a,{href:"https://www.typescriptlang.org/",children:"Typescript"}),", with a help of the tooling provided by ",(0,o.jsx)(t.a,{href:"https://vitejs.dev/",children:"Vite"}),". The frontend is designed as a Single-Page Application (SPA) that communicates with the backend through a REST API."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e For the backend, we employ Python's ",(0,o.jsx)(t.a,{href:"https://www.djangoproject.com/",children:"Django framework"}),", complemented by ",(0,o.jsx)(t.a,{href:"https://www.django-rest-framework.org/",children:"Django REST Framework"})," to implement the server API. The backend relies on ",(0,o.jsx)(t.a,{href:"https://www.postgresql.org/",children:"PostgreSQL"})," as its primary database."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e Centrifugo will handle WebSocket connections, providing a real-time transport layer for delivering events instantly to users. The backend will communicate with Centrifugo synchronously over Centrifugo HTTP API, and asynchronously using transactional outbox or CDC approach with ",(0,o.jsx)(t.a,{href:"https://docs.confluent.io/platform/current/connect/index.html",children:"Kafka Connect"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e ",(0,o.jsx)(t.a,{href:"https://www.nginx.com/",children:"Nginx"})," acts as a reverse proxy for all public endpoints of the app, facilitating the serving of frontend and backend endpoints from the same domain. This configuration is essential for secure HTTP-only cookie authentication of frontend-to-backend communication."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e To handle connection authentication in Centrifugo and perform channel permission checks, we use ",(0,o.jsx)(t.a,{href:"https://auth0.com/docs/secure/tokens/json-web-tokens",children:"JWT"})," (JSON Web Token) in the app. This ensures secure real-time communication and helps the backend to deal with a reconnect storm \u2013 a problem which becomes very important at scale in WebSocket applications that deal with many real-time connections."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{src:n(13338).Z+"",width:"5177",height:"682"})}),"\n",(0,o.jsx)(t.p,{children:"The tutorial is quite lengthy, and it will likely grow larger over time. The primary objective here is to illustrate the process of building a real-time app in detail. Even if you are not familiar with Django or React but wish to grasp Centrifugo concepts, consider reading this tutorial. After going through the entire content, you should feel much more comfortable with Centrifugo design and idiomatic approach to integrate with it."}),"\n",(0,o.jsx)(t.h2,{id:"straight-to-the-source-code",children:"Straight to the source code"}),"\n",(0,o.jsxs)(t.p,{children:["The complete source code for the app we build ",(0,o.jsx)(t.a,{href:"https://github.com/centrifugal/grand-chat-tutorial",children:"may be found on Github"}),". If you have Docker, you will be able to run the app locally quickly using just a few Docker Compose commands."]}),"\n",(0,o.jsxs)(t.p,{children:["If certain steps in the tutorial appear unclear, remember that you can refer to the source code. Or ask in ",(0,o.jsx)(t.a,{href:"/docs/getting-started/community",children:"our communities"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"centrifugo-vs-django-channels",children:"Centrifugo vs Django Channels"}),"\n",(0,o.jsxs)(t.p,{children:["Before we begin, a brief note about Django and real-time: Python developers are likely familiar with Django's popular framework for building real-time applications \u2013 ",(0,o.jsx)(t.a,{href:"https://channels.readthedocs.io/en/latest/",children:"Django Channels"}),". However, with Centrifugo, you can gain several important advantages:"]}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 More features out-of-the-box, including a history cache, missed message recovery, online presence, admin web UI, excellent observability, support for more real-time transports, Protobuf protocol, etc."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 Centrifugo serves as a universal real-time component, allowing you to decouple your real-time transport layer from the application core. You can integrate Centrifugo into any of your future projects, regardless of the programming language used in the backend."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 It's possible to use a traditional Django approach for writing application business logic \u2014 there's no need to use ASGI if you prefer not to. Centrifugo is easy to integrate into existing Django applications working on top of WSGI. And of course it's possible to combine using ASGI in Django with Centrifugo integration."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 You get an amazing scalable performance. Centrifugo is fast and supports sharding by channel to scale further. The use of JWT for authentication and channel authorization enables handling millions of concurrent connections with a reasonable number of Django backend instances. We will demonstrate that achieving chat rooms with tens of thousands of online users and minimal delivery latency is straightforward with Centrifugo. This is something Django Channels users might find challenging without investing considerable time in thinking about how to scale the app properly."})]})}function d(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},13338:(e,t,n)=>{n.d(t,{Z:()=>o});const o=n.p+"assets/images/grand-chat-tutorial-tech-1140e26ae44b73a07320b1ec477a318b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(67294);const a={},i=o.createContext(a);function r(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7287],{69608:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>s,toc:()=>l});var o=n(85893),a=n(11151);const i={id:"intro",sidebar_label:"Real-time app from scratch",title:"Building WebSocket chat (messenger) app from scratch"},r=void 0,s={id:"tutorial/intro",title:"Building WebSocket chat (messenger) app from scratch",description:"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.",source:"@site/docs/tutorial/intro.md",sourceDirName:"tutorial",slug:"/tutorial/intro",permalink:"/docs/tutorial/intro",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/intro.md",tags:[],version:"current",frontMatter:{id:"intro",sidebar_label:"Real-time app from scratch",title:"Building WebSocket chat (messenger) app from scratch"},sidebar:"Tutorial",next:{title:"App layout and behavior",permalink:"/docs/tutorial/layout"}},c={},l=[{value:"Application tech stack",id:"application-tech-stack",level:2},{value:"Straight to the source code",id:"straight-to-the-source-code",level:2},{value:"Centrifugo vs Django Channels",id:"centrifugo-vs-django-channels",level:2}];function h(e){const t={a:"a",h2:"h2",img:"img",p:"p",strong:"strong",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"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."}),"\n",(0,o.jsxs)(t.p,{children:["The app we build here is a WebSocket chat called ",(0,o.jsx)(t.strong,{children:"GrandChat"}),". The internet is full of chat tutorials, but we promise \u2013 here, we go beyond the usual basics. GrandChat is not just a set of isolated chat rooms but more like a messenger application, a simplified version of Discord, Telegram, or Slack. Here is a short demo of our final result:"]}),"\n",(0,o.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:!0,src:"/img/grand-chat-tutorial-demo.mp4"}),"\n",(0,o.jsx)(t.p,{children:"Note that we have a real-time synchronization across the app \u2013 room membership events and room messages are sent in real-time. Our design allows users to be subscribed to many rooms and receive updates from all of them within one screen. To achieve this in a scalable way we use individual channel for each application user. We will show how the app scales when there are thousands of room members to prove that with almost no additional effort it may scale to the size comparable to the largest Slack messenger installations with reasonable latency properties."}),"\n",(0,o.jsx)(t.h2,{id:"application-tech-stack",children:"Application tech stack"}),"\n",(0,o.jsx)(t.p,{children:"Centrifugo is completely agnostic to the technology stack, seamlessly integrating with any frontend or backend technologies. However, for the purpose of this tutorial, we needed to choose specific technologies to illustrate the entire process of building a real-time WebSocket app:"}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e On the frontend, we utilize ",(0,o.jsx)(t.a,{href:"https://react.dev/",children:"React"})," and ",(0,o.jsx)(t.a,{href:"https://www.typescriptlang.org/",children:"Typescript"}),", with a help of the tooling provided by ",(0,o.jsx)(t.a,{href:"https://vitejs.dev/",children:"Vite"}),". The frontend is designed as a Single-Page Application (SPA) that communicates with the backend through a REST API."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e For the backend, we employ Python's ",(0,o.jsx)(t.a,{href:"https://www.djangoproject.com/",children:"Django framework"}),", complemented by ",(0,o.jsx)(t.a,{href:"https://www.django-rest-framework.org/",children:"Django REST Framework"})," to implement the server API. The backend relies on ",(0,o.jsx)(t.a,{href:"https://www.postgresql.org/",children:"PostgreSQL"})," as its primary database."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e Centrifugo will handle WebSocket connections, providing a real-time transport layer for delivering events instantly to users. The backend will communicate with Centrifugo synchronously over Centrifugo HTTP API, and asynchronously using transactional outbox or CDC approach with ",(0,o.jsx)(t.a,{href:"https://docs.confluent.io/platform/current/connect/index.html",children:"Kafka Connect"}),"."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e ",(0,o.jsx)(t.a,{href:"https://www.nginx.com/",children:"Nginx"})," acts as a reverse proxy for all public endpoints of the app, facilitating the serving of frontend and backend endpoints from the same domain. This configuration is essential for secure HTTP-only cookie authentication of frontend-to-backend communication."]}),"\n",(0,o.jsxs)(t.p,{children:["\ud83d\udc8e To handle connection authentication in Centrifugo and perform channel permission checks, we use ",(0,o.jsx)(t.a,{href:"https://auth0.com/docs/secure/tokens/json-web-tokens",children:"JWT"})," (JSON Web Token) in the app. This ensures secure real-time communication and helps the backend to deal with a reconnect storm \u2013 a problem which becomes very important at scale in WebSocket applications that deal with many real-time connections."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{src:n(30707).Z+"",width:"5177",height:"682"})}),"\n",(0,o.jsx)(t.p,{children:"The tutorial is quite lengthy, and it will likely grow larger over time. The primary objective here is to illustrate the process of building a real-time app in detail. Even if you are not familiar with Django or React but wish to grasp Centrifugo concepts, consider reading this tutorial. After going through the entire content, you should feel much more comfortable with Centrifugo design and idiomatic approach to integrate with it."}),"\n",(0,o.jsx)(t.h2,{id:"straight-to-the-source-code",children:"Straight to the source code"}),"\n",(0,o.jsxs)(t.p,{children:["The complete source code for the app we build ",(0,o.jsx)(t.a,{href:"https://github.com/centrifugal/grand-chat-tutorial",children:"may be found on Github"}),". If you have Docker, you will be able to run the app locally quickly using just a few Docker Compose commands."]}),"\n",(0,o.jsxs)(t.p,{children:["If certain steps in the tutorial appear unclear, remember that you can refer to the source code. Or ask in ",(0,o.jsx)(t.a,{href:"/docs/getting-started/community",children:"our communities"}),"."]}),"\n",(0,o.jsx)(t.h2,{id:"centrifugo-vs-django-channels",children:"Centrifugo vs Django Channels"}),"\n",(0,o.jsxs)(t.p,{children:["Before we begin, a brief note about Django and real-time: Python developers are likely familiar with Django's popular framework for building real-time applications \u2013 ",(0,o.jsx)(t.a,{href:"https://channels.readthedocs.io/en/latest/",children:"Django Channels"}),". However, with Centrifugo, you can gain several important advantages:"]}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 More features out-of-the-box, including a history cache, missed message recovery, online presence, admin web UI, excellent observability, support for more real-time transports, Protobuf protocol, etc."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 Centrifugo serves as a universal real-time component, allowing you to decouple your real-time transport layer from the application core. You can integrate Centrifugo into any of your future projects, regardless of the programming language used in the backend."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 It's possible to use a traditional Django approach for writing application business logic \u2014 there's no need to use ASGI if you prefer not to. Centrifugo is easy to integrate into existing Django applications working on top of WSGI. And of course it's possible to combine using ASGI in Django with Centrifugo integration."}),"\n",(0,o.jsx)(t.p,{children:"\ud83d\udd25 You get an amazing scalable performance. Centrifugo is fast and supports sharding by channel to scale further. The use of JWT for authentication and channel authorization enables handling millions of concurrent connections with a reasonable number of Django backend instances. We will demonstrate that achieving chat rooms with tens of thousands of online users and minimal delivery latency is straightforward with Centrifugo. This is something Django Channels users might find challenging without investing considerable time in thinking about how to scale the app properly."})]})}function d(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},30707:(e,t,n)=>{n.d(t,{Z:()=>o});const o=n.p+"assets/images/grand-chat-tutorial-tech-1140e26ae44b73a07320b1ec477a318b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(67294);const a={},i=o.createContext(a);function r(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2eb9c429.95a18056.js b/assets/js/2eb9c429.313fb402.js similarity index 98% rename from assets/js/2eb9c429.95a18056.js rename to assets/js/2eb9c429.313fb402.js index 560055570..d339f812e 100644 --- a/assets/js/2eb9c429.95a18056.js +++ b/assets/js/2eb9c429.313fb402.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4968],{39012:(c,l,s)=>{s.r(l),s.d(l,{default:()=>v});s(67294);var h=s(85893);function v(){return(0,h.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"191",height:"55",viewBox:"0 0 191 55",className:"site-brand__logo",children:(0,h.jsxs)("g",{fill:"#000000",children:[(0,h.jsx)("g",{children:(0,h.jsx)("path",{d:"M89.348 16.818l-4.585-12.18c-.19-.473-.326-.892-.326-1.254 0-.948.734-1.255 2.88-1.338V.847h-8.07v1.2c.87.139 1.549.306 1.875 1.142l.97 2.562-4.267 11.039-4.423-12.152c-.19-.501-.326-.892-.326-1.254 0-.948.734-1.255 2.88-1.338V.847h-8.234v1.2c.87.139 1.55.306 1.875 1.142l7.005 18.48h.924l5.408-13.8 5.38 13.8h.925l6.626-18.48c.326-.864 1.006-1.003 1.875-1.143V.847h-6.848v1.2c2.147.083 2.908.417 2.908 1.365 0 .362-.163.78-.327 1.255l-4.125 12.15zM107.986 17.04c-.897 1.144-2.419 2.203-4.675 2.203-3.587 0-5.218-2.37-5.218-6.217h10.653v-.78c0-3.345-2.065-5.91-6.033-5.91-4.158 0-7.202 3.262-7.202 8.14 0 3.987 2.582 7.192 6.958 7.192 3.832 0 5.707-2.23 6.305-4.265l-.788-.362zm-5.952-8.975c2.446 0 3.723 1.616 3.723 3.568h-7.582c.3-2.091 1.712-3.568 3.86-3.568M51.395 17.04c-.897 1.144-2.42 2.203-4.675 2.203-3.587 0-5.218-2.37-5.218-6.217h10.654v-.78c0-3.345-2.066-5.91-6.033-5.91-4.158 0-7.202 3.262-7.202 8.14 0 3.987 2.581 7.192 6.957 7.192 3.831 0 5.707-2.23 6.305-4.265l-.788-.362zm-5.952-8.975c2.446 0 3.723 1.616 3.723 3.568h-7.582c.299-2.091 1.712-3.568 3.859-3.568M30.782 6.336c-2.12 0-4.022 1.06-5.191 2.592V6.336h-.815L21.63 7.73v.669l1.243.976v15.377c0 1.281-.924 1.42-2.174 1.588v1.087h7.066V26.34c-1.25-.168-2.174-.307-2.174-1.59v-4.197c.87.669 2.12 1.115 3.75 1.115 4.294 0 7.827-3.206 7.827-8.363 0-3.847-2.202-6.969-6.386-6.969zM29.64 20.08c-2.062 0-4.043-.862-4.049-3.5v-6.285c.924-1.003 2.5-1.84 4.158-1.84 3.505 0 4.62 3.123 4.62 6.022 0 3.596-1.875 5.603-4.729 5.603zM10.488.607C4.765.607.017 4.82.017 11.532c0 5.318 3.772 10.136 9.906 10.136 5.723 0 10.471-4.213 10.471-10.925 0-5.318-3.772-10.136-9.906-10.136m6.511 11.215c0 4.908-2.328 7.837-6.229 7.837-2.324 0-4.244-.967-5.55-2.798-1.166-1.635-1.808-3.91-1.808-6.408 0-4.907 2.329-7.837 6.229-7.837 2.325 0 4.244.968 5.55 2.799C16.357 7.05 17 9.325 17 11.822M68.392 18.713v-7.805c0-3.262-1.875-4.572-4.538-4.572-2.146 0-4.319 1.393-5.515 2.73v-2.73h-.815L54.38 7.73v.669l1.243.976v9.406c-.034 1.217-.945 1.357-2.171 1.521v1.087h7.066v-1.087c-1.25-.167-2.174-.307-2.174-1.589h-.004v-8.392c1.142-.947 3.015-1.727 4.482-1.727 1.794 0 2.854.641 2.854 2.704v7.415c0 1.282-.924 1.422-2.174 1.589v1.087h7.066v-1.087c-1.251-.167-2.175-.307-2.175-1.589zM119.036 6.336c-2.12 0-3.968 1.06-5.164 2.593V.02h-.815l-3.146 1.394v.669l1.243.976v16.743h.003c1.169.92 3.423 1.866 6.465 1.866 4.267 0 7.827-3.206 7.827-8.363 0-3.847-2.228-6.969-6.413-6.969zM117.92 20.08c-2.064 0-4.047-.863-4.049-3.508v-6.277c.924-1.003 2.5-1.84 4.158-1.84 3.479 0 4.62 3.123 4.62 6.022 0 3.596-1.875 5.603-4.729 5.603z",transform:"translate(64.687 15.682)"})}),(0,h.jsx)("path",{d:"M40.828 27.457l-.001.055 6.38 3.517s1.855 1.048 3.889 1.808c-2.163-.193-4.29-.05-4.29-.05l-7.277.41c-.265.554-.569 1.087-.906 1.596l11.096 8.805c.487-.666.945-1.355 1.37-2.065l-5.016-3.985c-1.95-1.616-3.903-2.52-3.903-2.52 1.817.37 4.435.21 4.435.21l7.373-.414c.23-.829.425-1.673.578-2.53l-6.463-3.56s-2.288-1.277-4.086-1.729c0 0 2.152.03 4.613-.584l6.25-1.42c-.073-.83-.182-1.649-.328-2.455l-13.835 3.138c.078.58.121 1.172.121 1.773zM37.1 36.694c-.424.44-.878.852-1.358 1.231l1.22 7.168s.334 2.099 1.005 4.157c-1.197-1.806-2.635-3.375-2.635-3.375l-4.86-5.417c-.591.136-1.197.235-1.815.29l.011 14.137c.835-.036 1.66-.11 2.475-.22l-.002-6.392c.052-2.528-.457-4.613-.457-4.613.842 1.647 2.6 3.586 2.6 3.586l4.923 5.489c.801-.34 1.584-.716 2.345-1.128l-1.237-7.255s-.426-2.58-1.192-4.263c0 0 1.318 1.696 3.334 3.231l5.012 3.986c.599-.57 1.17-1.168 1.716-1.79L37.1 36.694zM35.827 17.056l6.738-2.78s1.978-.792 3.843-1.903c-1.501 1.565-2.715 3.312-2.715 3.312l-4.215 5.926c.268.547.497 1.115.689 1.7l13.828-3.157c-.22-.8-.477-1.586-.767-2.356l-6.257 1.427c-2.484.511-4.41 1.47-4.41 1.47 1.423-1.185 2.929-3.326 2.929-3.326l4.27-5.999c-.511-.703-1.054-1.38-1.628-2.03l-6.816 2.814s-2.428.988-3.903 2.108c0 0 1.366-1.659 2.418-3.96l2.784-5.759c-.69-.455-1.4-.88-2.132-1.272l-6.162 12.735c.527.316 1.03.668 1.506 1.05zM15.393 21.612l-4.216-5.925s-1.214-1.747-2.715-3.311c1.865 1.11 3.844 1.901 3.844 1.901l6.737 2.78c.477-.383.979-.734 1.506-1.05L14.385 3.272c-.73.393-1.441.818-2.131 1.273l2.785 5.759c1.052 2.3 2.419 3.959 2.419 3.959-1.476-1.12-3.905-2.107-3.905-2.107L6.737 9.342c-.573.65-1.116 1.328-1.627 2.03l4.27 6s1.506 2.14 2.93 3.325c0 0-1.926-.959-4.41-1.47l-6.257-1.425c-.29.77-.547 1.555-.768 2.355l13.83 3.156c.19-.585.42-1.154.688-1.7zM24.402 40.458l-4.86 5.418s-1.438 1.57-2.634 3.376c.67-2.06 1.004-4.158 1.004-4.158l1.22-7.168c-.481-.38-.935-.79-1.359-1.231L6.69 45.518c.545.622 1.118 1.22 1.717 1.79l5.011-3.986c2.015-1.535 3.333-3.231 3.333-3.231-.766 1.683-1.19 4.262-1.19 4.262l-1.237 7.256c.76.412 1.543.788 2.345 1.128l4.921-5.49s1.758-1.94 2.6-3.586c0 0-.509 2.085-.456 4.612l-.001 6.393c.814.11 1.64.183 2.474.22l.01-14.137c-.62-.056-1.224-.154-1.815-.29zM24.511 14.43l2.02-6.981s.613-2.036.904-4.181c.293 2.145.905 4.18.905 4.18l2.02 6.983c.605.134 1.194.307 1.764.52L38.27 2.206c-.758-.324-1.535-.614-2.326-.87L33.16 7.1c-1.148 2.255-1.597 4.354-1.597 4.354-.042-1.848-.782-4.356-.782-4.356L28.739.032C28.307.012 27.873 0 27.436 0c-.437 0-.872.011-1.304.032L24.09 7.099s-.74 2.508-.782 4.356c0 0-.449-2.099-1.597-4.354l-2.783-5.764c-.792.256-1.568.546-2.326.87l6.146 12.743c.57-.212 1.16-.385 1.763-.52zM6.252 26.424c2.46.614 4.613.583 4.613.583-1.798.452-4.087 1.73-4.087 1.73l-6.462 3.56c.153.858.347 1.702.579 2.53l7.373.413s2.618.161 4.434-.21c0 0-1.952.903-3.902 2.52l-5.017 3.986c.427.71.884 1.399 1.371 2.065l11.095-8.807c-.337-.509-.64-1.041-.907-1.596l-7.276-.41s-2.127-.141-4.29.053c2.033-.762 3.888-1.81 3.888-1.81l6.38-3.517-.002-.057c0-.601.044-1.192.121-1.772L.33 22.55c-.146.807-.255 1.626-.329 2.455l6.252 1.42z"})]})})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4968],{52537:(c,l,s)=>{s.r(l),s.d(l,{default:()=>v});s(67294);var h=s(85893);function v(){return(0,h.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"191",height:"55",viewBox:"0 0 191 55",className:"site-brand__logo",children:(0,h.jsxs)("g",{fill:"#000000",children:[(0,h.jsx)("g",{children:(0,h.jsx)("path",{d:"M89.348 16.818l-4.585-12.18c-.19-.473-.326-.892-.326-1.254 0-.948.734-1.255 2.88-1.338V.847h-8.07v1.2c.87.139 1.549.306 1.875 1.142l.97 2.562-4.267 11.039-4.423-12.152c-.19-.501-.326-.892-.326-1.254 0-.948.734-1.255 2.88-1.338V.847h-8.234v1.2c.87.139 1.55.306 1.875 1.142l7.005 18.48h.924l5.408-13.8 5.38 13.8h.925l6.626-18.48c.326-.864 1.006-1.003 1.875-1.143V.847h-6.848v1.2c2.147.083 2.908.417 2.908 1.365 0 .362-.163.78-.327 1.255l-4.125 12.15zM107.986 17.04c-.897 1.144-2.419 2.203-4.675 2.203-3.587 0-5.218-2.37-5.218-6.217h10.653v-.78c0-3.345-2.065-5.91-6.033-5.91-4.158 0-7.202 3.262-7.202 8.14 0 3.987 2.582 7.192 6.958 7.192 3.832 0 5.707-2.23 6.305-4.265l-.788-.362zm-5.952-8.975c2.446 0 3.723 1.616 3.723 3.568h-7.582c.3-2.091 1.712-3.568 3.86-3.568M51.395 17.04c-.897 1.144-2.42 2.203-4.675 2.203-3.587 0-5.218-2.37-5.218-6.217h10.654v-.78c0-3.345-2.066-5.91-6.033-5.91-4.158 0-7.202 3.262-7.202 8.14 0 3.987 2.581 7.192 6.957 7.192 3.831 0 5.707-2.23 6.305-4.265l-.788-.362zm-5.952-8.975c2.446 0 3.723 1.616 3.723 3.568h-7.582c.299-2.091 1.712-3.568 3.859-3.568M30.782 6.336c-2.12 0-4.022 1.06-5.191 2.592V6.336h-.815L21.63 7.73v.669l1.243.976v15.377c0 1.281-.924 1.42-2.174 1.588v1.087h7.066V26.34c-1.25-.168-2.174-.307-2.174-1.59v-4.197c.87.669 2.12 1.115 3.75 1.115 4.294 0 7.827-3.206 7.827-8.363 0-3.847-2.202-6.969-6.386-6.969zM29.64 20.08c-2.062 0-4.043-.862-4.049-3.5v-6.285c.924-1.003 2.5-1.84 4.158-1.84 3.505 0 4.62 3.123 4.62 6.022 0 3.596-1.875 5.603-4.729 5.603zM10.488.607C4.765.607.017 4.82.017 11.532c0 5.318 3.772 10.136 9.906 10.136 5.723 0 10.471-4.213 10.471-10.925 0-5.318-3.772-10.136-9.906-10.136m6.511 11.215c0 4.908-2.328 7.837-6.229 7.837-2.324 0-4.244-.967-5.55-2.798-1.166-1.635-1.808-3.91-1.808-6.408 0-4.907 2.329-7.837 6.229-7.837 2.325 0 4.244.968 5.55 2.799C16.357 7.05 17 9.325 17 11.822M68.392 18.713v-7.805c0-3.262-1.875-4.572-4.538-4.572-2.146 0-4.319 1.393-5.515 2.73v-2.73h-.815L54.38 7.73v.669l1.243.976v9.406c-.034 1.217-.945 1.357-2.171 1.521v1.087h7.066v-1.087c-1.25-.167-2.174-.307-2.174-1.589h-.004v-8.392c1.142-.947 3.015-1.727 4.482-1.727 1.794 0 2.854.641 2.854 2.704v7.415c0 1.282-.924 1.422-2.174 1.589v1.087h7.066v-1.087c-1.251-.167-2.175-.307-2.175-1.589zM119.036 6.336c-2.12 0-3.968 1.06-5.164 2.593V.02h-.815l-3.146 1.394v.669l1.243.976v16.743h.003c1.169.92 3.423 1.866 6.465 1.866 4.267 0 7.827-3.206 7.827-8.363 0-3.847-2.228-6.969-6.413-6.969zM117.92 20.08c-2.064 0-4.047-.863-4.049-3.508v-6.277c.924-1.003 2.5-1.84 4.158-1.84 3.479 0 4.62 3.123 4.62 6.022 0 3.596-1.875 5.603-4.729 5.603z",transform:"translate(64.687 15.682)"})}),(0,h.jsx)("path",{d:"M40.828 27.457l-.001.055 6.38 3.517s1.855 1.048 3.889 1.808c-2.163-.193-4.29-.05-4.29-.05l-7.277.41c-.265.554-.569 1.087-.906 1.596l11.096 8.805c.487-.666.945-1.355 1.37-2.065l-5.016-3.985c-1.95-1.616-3.903-2.52-3.903-2.52 1.817.37 4.435.21 4.435.21l7.373-.414c.23-.829.425-1.673.578-2.53l-6.463-3.56s-2.288-1.277-4.086-1.729c0 0 2.152.03 4.613-.584l6.25-1.42c-.073-.83-.182-1.649-.328-2.455l-13.835 3.138c.078.58.121 1.172.121 1.773zM37.1 36.694c-.424.44-.878.852-1.358 1.231l1.22 7.168s.334 2.099 1.005 4.157c-1.197-1.806-2.635-3.375-2.635-3.375l-4.86-5.417c-.591.136-1.197.235-1.815.29l.011 14.137c.835-.036 1.66-.11 2.475-.22l-.002-6.392c.052-2.528-.457-4.613-.457-4.613.842 1.647 2.6 3.586 2.6 3.586l4.923 5.489c.801-.34 1.584-.716 2.345-1.128l-1.237-7.255s-.426-2.58-1.192-4.263c0 0 1.318 1.696 3.334 3.231l5.012 3.986c.599-.57 1.17-1.168 1.716-1.79L37.1 36.694zM35.827 17.056l6.738-2.78s1.978-.792 3.843-1.903c-1.501 1.565-2.715 3.312-2.715 3.312l-4.215 5.926c.268.547.497 1.115.689 1.7l13.828-3.157c-.22-.8-.477-1.586-.767-2.356l-6.257 1.427c-2.484.511-4.41 1.47-4.41 1.47 1.423-1.185 2.929-3.326 2.929-3.326l4.27-5.999c-.511-.703-1.054-1.38-1.628-2.03l-6.816 2.814s-2.428.988-3.903 2.108c0 0 1.366-1.659 2.418-3.96l2.784-5.759c-.69-.455-1.4-.88-2.132-1.272l-6.162 12.735c.527.316 1.03.668 1.506 1.05zM15.393 21.612l-4.216-5.925s-1.214-1.747-2.715-3.311c1.865 1.11 3.844 1.901 3.844 1.901l6.737 2.78c.477-.383.979-.734 1.506-1.05L14.385 3.272c-.73.393-1.441.818-2.131 1.273l2.785 5.759c1.052 2.3 2.419 3.959 2.419 3.959-1.476-1.12-3.905-2.107-3.905-2.107L6.737 9.342c-.573.65-1.116 1.328-1.627 2.03l4.27 6s1.506 2.14 2.93 3.325c0 0-1.926-.959-4.41-1.47l-6.257-1.425c-.29.77-.547 1.555-.768 2.355l13.83 3.156c.19-.585.42-1.154.688-1.7zM24.402 40.458l-4.86 5.418s-1.438 1.57-2.634 3.376c.67-2.06 1.004-4.158 1.004-4.158l1.22-7.168c-.481-.38-.935-.79-1.359-1.231L6.69 45.518c.545.622 1.118 1.22 1.717 1.79l5.011-3.986c2.015-1.535 3.333-3.231 3.333-3.231-.766 1.683-1.19 4.262-1.19 4.262l-1.237 7.256c.76.412 1.543.788 2.345 1.128l4.921-5.49s1.758-1.94 2.6-3.586c0 0-.509 2.085-.456 4.612l-.001 6.393c.814.11 1.64.183 2.474.22l.01-14.137c-.62-.056-1.224-.154-1.815-.29zM24.511 14.43l2.02-6.981s.613-2.036.904-4.181c.293 2.145.905 4.18.905 4.18l2.02 6.983c.605.134 1.194.307 1.764.52L38.27 2.206c-.758-.324-1.535-.614-2.326-.87L33.16 7.1c-1.148 2.255-1.597 4.354-1.597 4.354-.042-1.848-.782-4.356-.782-4.356L28.739.032C28.307.012 27.873 0 27.436 0c-.437 0-.872.011-1.304.032L24.09 7.099s-.74 2.508-.782 4.356c0 0-.449-2.099-1.597-4.354l-2.783-5.764c-.792.256-1.568.546-2.326.87l6.146 12.743c.57-.212 1.16-.385 1.763-.52zM6.252 26.424c2.46.614 4.613.583 4.613.583-1.798.452-4.087 1.73-4.087 1.73l-6.462 3.56c.153.858.347 1.702.579 2.53l7.373.413s2.618.161 4.434-.21c0 0-1.952.903-3.902 2.52l-5.017 3.986c.427.71.884 1.399 1.371 2.065l11.095-8.807c-.337-.509-.64-1.041-.907-1.596l-7.276-.41s-2.127-.141-4.29.053c2.033-.762 3.888-1.81 3.888-1.81l6.38-3.517-.002-.057c0-.601.044-1.192.121-1.772L.33 22.55c-.146.807-.255 1.626-.329 2.455l6.252 1.42z"})]})})}}}]); \ No newline at end of file diff --git a/assets/js/2f70c421.08296a05.js b/assets/js/2f70c421.08296a05.js deleted file mode 100644 index f34eb9cd0..000000000 --- a/assets/js/2f70c421.08296a05.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4633],{30433:(e,n,i)=>{i.d(n,{Z:()=>o});i(67294);var s=i(36905);const t={tabItem:"tabItem_Ymn6"};var c=i(85893);function o(e){let{children:n,hidden:i,className:o}=e;return(0,c.jsx)("div",{role:"tabpanel",className:(0,s.Z)(t.tabItem,o),hidden:i,children:n})}},22808:(e,n,i)=>{i.d(n,{Z:()=>y});var s=i(67294),t=i(36905),c=i(63735),o=i(16550),r=i(20613),l=i(34423),a=i(20636),d=i(99200);function u(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:i}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:i,attributes:s,default:t}}=e;return{value:n,label:i,attributes:s,default:t}}))}(i);return function(e){const n=(0,a.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[n,i])}function p(e){let{value:n,tabValues:i}=e;return i.some((e=>e.value===n))}function b(e){let{queryString:n=!1,groupId:i}=e;const t=(0,o.k6)(),c=function(e){let{queryString:n=!1,groupId:i}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!i)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return i??null}({queryString:n,groupId:i});return[(0,l._X)(c),(0,s.useCallback)((e=>{if(!c)return;const n=new URLSearchParams(t.location.search);n.set(c,e),t.replace({...t.location,search:n.toString()})}),[c,t])]}function x(e){const{defaultValue:n,queryString:i=!1,groupId:t}=e,c=h(e),[o,l]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:i}=e;if(0===i.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(n){if(!p({value:n,tabValues:i}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${i.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=i.find((e=>e.default))??i[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:c}))),[a,u]=b({queryString:i,groupId:t}),[x,g]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,c]=(0,d.Nk)(i);return[t,(0,s.useCallback)((e=>{i&&c.set(e)}),[i,c])]}({groupId:t}),f=(()=>{const e=a??x;return p({value:e,tabValues:c})?e:null})();(0,r.Z)((()=>{f&&l(f)}),[f]);return{selectedValue:o,selectValue:(0,s.useCallback)((e=>{if(!p({value:e,tabValues:c}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),g(e)}),[u,g,c]),tabValues:c}}var g=i(5730);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=i(85893);function m(e){let{className:n,block:i,selectedValue:s,selectValue:o,tabValues:r}=e;const l=[],{blockElementScrollPositionUntilNextRender:a}=(0,c.o5)(),d=e=>{const n=e.currentTarget,i=l.indexOf(n),t=r[i].value;t!==s&&(a(n),o(t))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const i=l.indexOf(e.currentTarget)+1;n=l[i]??l[0];break}case"ArrowLeft":{const i=l.indexOf(e.currentTarget)-1;n=l[i]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":i},n),children:r.map((e=>{let{value:n,label:i,attributes:c}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...c,className:(0,t.Z)("tabs__item",f.tabItem,c?.className,{"tabs__item--active":s===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:t}=e;const c=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=c.find((e=>e.props.value===t));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:c.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function w(e){const n=x(e);return(0,j.jsxs)("div",{className:(0,t.Z)("tabs-container",f.tabList),children:[(0,j.jsx)(m,{...n,...e}),(0,j.jsx)(v,{...n,...e})]})}function y(e){const n=(0,g.Z)();return(0,j.jsx)(w,{...e,children:u(e.children)},String(n))}},75282:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>r,metadata:()=>a,toc:()=>u});var s=i(85893),t=i(11151),c=i(22808),o=i(30433);const r={id:"client_api",title:"Client SDK API"},l=void 0,a={id:"transports/client_api",title:"Client SDK API",description:"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.",source:"@site/versioned_docs/version-4/transports/client_api.md",sourceDirName:"transports",slug:"/transports/client_api",permalink:"/docs/4/transports/client_api",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/transports/client_api.md",tags:[],version:"4",frontMatter:{id:"client_api",title:"Client SDK API"},sidebar:"Transports",previous:{title:"Real-time transports",permalink:"/docs/4/transports/overview"},next:{title:"Client real-time SDKs",permalink:"/docs/4/transports/client_sdk"}},d={},u=[{value:"Client connection states",id:"client-connection-states",level:2},{value:"Client common options",id:"client-common-options",level:2},{value:"Client methods",id:"client-methods",level:2},{value:"Client connection token",id:"client-connection-token",level:2},{value:"Connection PING/PONG",id:"connection-pingpong",level:2},{value:"Subscription states",id:"subscription-states",level:2},{value:"Subscription management",id:"subscription-management",level:2},{value:"Listen to channel publications",id:"listen-to-channel-publications",level:2},{value:"Subscription recovery state",id:"subscription-recovery-state",level:2},{value:"Subscription common options",id:"subscription-common-options",level:2},{value:"Subscription methods",id:"subscription-methods",level:2},{value:"Subscription token",id:"subscription-token",level:2},{value:"Server-side subscriptions",id:"server-side-subscriptions",level:2},{value:"Error codes",id:"error-codes",level:2},{value:"Unsubscribe codes",id:"unsubscribe-codes",level:2},{value:"Disconnect codes",id:"disconnect-codes",level:2},{value:"RPC",id:"rpc",level:2},{value:"Channel history API",id:"channel-history-api",level:2},{value:"Presence and presence stats API",id:"presence-and-presence-stats-api",level:2},{value:"SDK common best practices",id:"sdk-common-best-practices",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/protocol/blob/master/definitions/client.proto",children:"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."]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"For Centrifugo v4 we introduced a new generation of SDKs for Javascript, Dart, Go, Swift, and Java \u2013 all based on updated client protocol and new client API iteration."})}),"\n",(0,s.jsxs)(n.p,{children:["This chapter describes the core concepts of client SDKs API. If you want to find out lower-level client protocol framing details then look at ",(0,s.jsx)(n.a,{href:"/docs/4/transports/client_protocol",children:"client protocol"})," document."]}),"\n",(0,s.jsxs)(n.p,{children:["Most examples here are written using our Javascript client (",(0,s.jsx)(n.code,{children:"centrifuge-js"}),"), but all other Centrifugo connectors now have very similar semantics and APIs very close to each other."]}),"\n",(0,s.jsx)(n.h2,{id:"client-connection-states",children:"Client connection states"}),"\n",(0,s.jsx)(n.p,{children:"Client connection has 4 states:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"disconnected"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"connecting"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"connected"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"closed"})}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"closed"})," state is only implemented by SDKs where it makes sense (need to clean up allocated resources when app gracefully shuts down \u2013 for example in Java SDK we close thread executors used internally)."]})}),"\n",(0,s.jsxs)(n.p,{children:["When a new Client is created it has a ",(0,s.jsx)(n.code,{children:"disconnected"})," state. To connect to a server ",(0,s.jsx)(n.code,{children:"connect()"})," method must be called. After calling connect Client moves to the ",(0,s.jsx)(n.code,{children:"connecting"})," state. If a Client can't connect to a server it attempts to create a connection with an exponential backoff algorithm (with ",(0,s.jsx)(n.a,{href:"https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/",children:"full jitter"}),"). If a connection to a server is successful then the state becomes ",(0,s.jsx)(n.code,{children:"connected"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["If a connection is lost (due to a missing network for example, or due to reconnect advice received from a server, or due to some client-side error that can't be recovered without reconnecting) Client goes to the ",(0,s.jsx)(n.code,{children:"connecting"})," state again. In this state Client tries to reconnect (again, with an exponential backoff algorithm)."]}),"\n",(0,s.jsxs)(n.p,{children:["The Client's state can become ",(0,s.jsx)(n.code,{children:"disconnected"}),". This happens when Client's ",(0,s.jsx)(n.code,{children:"disconnect()"})," method was called by a developer. Also, this can happen due to server advice from a server, or due to a terminal problem that happened on the client-side."]}),"\n",(0,s.jsx)(n.p,{children:"Here is a program where we create a Client instance, set callbacks to listen to state updates and establish a connection with a server:"}),"\n","\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nclient.on('connecting', function(ctx) {\n console.log('connecting', ctx);\n});\n\nclient.on('connected', function(ctx) {\n console.log('connected', ctx);\n});\n\nclient.on('disconnected', function(ctx) {\n console.log('disconnected', ctx);\n});\n\nclient.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final onEvent = (dynamic event) {\n print('client event> $event');\n};\n\nfinal client = centrifuge.createClient(\n 'ws://localhost:8000/connection/websocket',\n centrifuge.ClientConfig(),\n);\n\nclient.connecting.listen(onEvent);\nclient.connected.listen(onEvent);\nclient.disconnected.listen(onEvent);\n\nawait client.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'import SwiftCentrifuge\n\nclass ClientDelegate : NSObject, CentrifugeClientDelegate {\n func onConnecting(_ c: CentrifugeClient, _ e: CentrifugeConnectingEvent) {\n print("connecting", e.code, e.reason)\n }\n func onConnected(_ client: CentrifugeClient, _ e: CentrifugeConnectedEvent) {\n print("connected with id", e.client)\n }\n func onDisconnected(_ client: CentrifugeClient, _ e: CentrifugeDisconnectedEvent) {\n print("disconnected", e.code, e.reason)\n }\n}\n\nlet config = CentrifugeClientConfig()\nlet endpoint = "ws://localhost:8000/connection/websocket"\nlet client = CentrifugeClient(endpoint: endpoint, config: config, delegate: ClientDelegate())\nclient.connect()\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'EventListener listener = new EventListener() {\n @Override\n public void onConnected(Client client, ConnectedEvent event) {\n System.out.println("connected");\n }\n @Override\n public void onConnecting(Client client, ConnectingEvent event) {\n System.out.printf("connecting: %s%n", event.getReason());\n }\n @Override\n public void onDisconnected(Client client, DisconnectedEvent event) {\n System.out.printf("disconnected %d %s", event.getCode(), event.getReason());\n }\n};\n\nOptions opts = new Options();\n\nClient client = new Client("ws://localhost:8000/connection/websocket", opts, listener);\n\nclient.connect();\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'client := centrifuge.NewJsonClient(\n "ws://localhost:8000/connection/websocket",\n centrifuge.Config{},\n)\ndefer client.Close()\n\nclient.OnConnecting(func(e centrifuge.ConnectingEvent) {\n log.Printf("Connecting - %d (%s)", e.Code, e.Reason)\n})\nclient.OnConnected(func(e centrifuge.ConnectedEvent) {\n log.Printf("Connected with ID %s", e.ClientID)\n})\nclient.OnDisconnected(func(e centrifuge.DisconnectedEvent) {\n log.Printf("Disconnected: %d (%s)", e.Code, e.Reason)\n})\n\n_ = client.connect()\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"In case of successful connection Client states will transition like this:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"disconnected"})," (initial) -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"connected"})," (",(0,s.jsx)(n.code,{children:"on('connected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client temporary lost a connection with a server and then successfully reconnected:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"connected"})," (",(0,s.jsx)(n.code,{children:"on('connected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client temporary lost a connection with a server, but got a terminal error upon reconnection:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"disconnected"})," (",(0,s.jsx)(n.code,{children:"on('disconnected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client came across terminal condition (for example, if during a connection token refresh application found that user has no permission to connect anymore):"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"disconnected"})," (",(0,s.jsx)(n.code,{children:"on('disconnected')"})," called)."]}),"\n",(0,s.jsxs)(n.p,{children:["Both ",(0,s.jsx)(n.code,{children:"connecting"})," and ",(0,s.jsx)(n.code,{children:"disconnected"})," events have numeric ",(0,s.jsx)(n.code,{children:"code"})," and human-readable string ",(0,s.jsx)(n.code,{children:"reason"})," in their context, so you can look at them and find the exact reason why the Client went to the ",(0,s.jsx)(n.code,{children:"connecting"})," state or to the ",(0,s.jsx)(n.code,{children:"disconnected"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"This diagram demonstrates possible Client state transitions:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifugo scheme",src:i(77262).Z+"",width:"2352",height:"1700"})}),"\n",(0,s.jsxs)(n.p,{children:["You can also listen for all errors happening internally (which are not reflected by state changes, for example, transport errors happening on initial connect, transport during reconnect, connection token refresh related errors, etc) while the client works by using ",(0,s.jsx)(n.code,{children:"error"})," event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"client.on('error', function(ctx) {\n console.log('client error', ctx);\n});\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to disconnect from a server call ",(0,s.jsx)(n.code,{children:".disconnect()"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"client.disconnect();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case ",(0,s.jsx)(n.code,{children:"on('disconnected')"})," will be called. You can call ",(0,s.jsx)(n.code,{children:"connect()"})," again when you need to establish a connection."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"closed"})," state implemented in SDKs where resources like internal queues, thread executors, etc must be cleaned up when the Client is not needed anymore. All subscriptions should automatically go to the ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state upon closing. The client is not usable after going to a ",(0,s.jsx)(n.code,{children:"closed"})," state."]}),"\n",(0,s.jsx)(n.h2,{id:"client-common-options",children:"Client common options"}),"\n",(0,s.jsx)(n.p,{children:"There are several common options available when creating Client instance."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["option to set connection token and callback to get connection token upon expiration (see below ",(0,s.jsx)(n.a,{href:"#client-connection-token",children:"mode details"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"option to set connect data"}),"\n",(0,s.jsx)(n.li,{children:"option to configure operation timeout"}),"\n",(0,s.jsx)(n.li,{children:"tweaks for reconnect backoff algorithm (min delay, max delay)"}),"\n",(0,s.jsx)(n.li,{children:"configure max delay of server pings (to detect broken connection)"}),"\n",(0,s.jsxs)(n.li,{children:["configure headers to send in WebSocket upgrade request (except ",(0,s.jsx)(n.code,{children:"centrifuge-js"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"configure client name and version for analytics purpose"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"client-methods",children:"Client methods"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"connect()"})," \u2013 connect to a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"disconnect()"})," - disconnect from a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"close()"})," - close Client if not needed anymore"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"send(data)"})," - send asynchronous message to a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rpc(method, data)"})," - send arbitrary RPC and wait for response"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"client-connection-token",children:"Client connection token"}),"\n",(0,s.jsx)(n.p,{children:"All SDKs support connecting to Centrifugo with JWT. Initial connection token can be set in Client option upon initialization. Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE'\n});\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the token sets connection expiration then the client SDK will keep the token refreshed. It does this by calling a special callback function. This callback must return a new token. If a new token with updated connection expiration is returned from callback then it's sent to Centrifugo. If your callback returns an empty string \u2013 this means the user has no permission to connect to Centrifugo and the Client will move to a disconnected state. In case of error returned by your callback SDK will retry the operation after some jittered time."}),"\n",(0,s.jsx)(n.p,{children:"An example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"function getToken(url, ctx) {\n return new Promise((resolve, reject) => {\n fetch(url, {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify(ctx)\n })\n .then(res => {\n if (!res.ok) {\n throw new Error(`Unexpected status code ${res.status}`);\n }\n return res.json();\n })\n .then(data => {\n resolve(data.token);\n })\n .catch(err => {\n reject(err);\n });\n });\n}\n\nconst client = new Centrifuge(\n 'ws://localhost:8000/connection/websocket',\n {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE',\n getToken: function (ctx) {\n return getToken('/centrifuge/connection_token', ctx);\n }\n }\n);\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["If initial token is not provided, but ",(0,s.jsx)(n.code,{children:"getToken"})," is specified \u2013 then SDK should assume that developer wants to use token authentication. In this case SDK should attempt to get a connection token before establishing an initial connection."]})}),"\n",(0,s.jsx)(n.h2,{id:"connection-pingpong",children:"Connection PING/PONG"}),"\n",(0,s.jsx)(n.p,{children:"PINGs sent by a server, a client should answer with PONGs upon receiving PING. If a client does not receive PING from a server for a long time (ping interval + configured delay) \u2013 the connection is considered broken and will be re-established."}),"\n",(0,s.jsx)(n.h2,{id:"subscription-states",children:"Subscription states"}),"\n",(0,s.jsxs)(n.p,{children:["Client allows subscribing on channels. This can be done by creating ",(0,s.jsx)(n.code,{children:"Subscription"})," object."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription(channel);\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["When a",(0,s.jsx)(n.code,{children:"newSubscription"})," method is called Client allocates a new Subscription instance and saves it in the internal subscription registry. Having a registry of allocated subscriptions allows SDK to manage resubscribes upon reconnecting to a server. Centrifugo connectors do not allow creating two subscriptions to the same channel \u2013 in this case, ",(0,s.jsx)(n.code,{children:"newSubscription"})," can throw an exception."]}),"\n",(0,s.jsx)(n.p,{children:"Subscription has 3 states:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"unsubscribed"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"subscribing"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"subscribed"})}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["When a new Subscription is created it has an ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state."]}),"\n",(0,s.jsxs)(n.p,{children:["To initiate the actual process of subscribing to a channel ",(0,s.jsx)(n.code,{children:"subscribe()"})," method of Subscription instance should be called. After calling ",(0,s.jsx)(n.code,{children:"subscribe()"})," Subscription moves to ",(0,s.jsx)(n.code,{children:"subscribing"})," state."]}),"\n",(0,s.jsxs)(n.p,{children:["If subscription to a channel is not successful then depending on error type subscription can automatically resubscribe (with exponential backoff) or go to an ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state (upon non-temporary error). If subscription to a channel is successful then the state becomes ",(0,s.jsx)(n.code,{children:"subscribed"}),"."]}),"\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = client.newSubscription(channel);\n\nsub.on('subscribing', function(ctx) {\n console.log('subscribing');\n});\n\nsub.on('subscribed', function(ctx) {\n console.log('subscribed');\n});\n\nsub.on('unsubscribed', function(ctx) {\n console.log('unsubscribed');\n});\n\nsub.subscribe();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final onSubscriptionEvent = (dynamic event) async {\n print('subscription $channel> $event');\n};\n\nfinal subscription = client.newSubscription(channel);\n\nsubscription.subscribing.listen(onSubscriptionEvent);\nsubscription.subscribed.listen(onSubscriptionEvent);\nsubscription.unsubscribed.listen(onSubscriptionEvent);\n\nawait subscription.subscribe();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'class SubscriptionDelegate : NSObject, CentrifugeSubscriptionDelegate {\n func onSubscribing(_ s: CentrifugeSubscription, _ e: CentrifugeSubscribingEvent) {\n print("subscribing", e.code, e.reason)\n }\n func onSubscribed(_ s: CentrifugeSubscription, _ e: CentrifugeSubscribedEvent) {\n print("subscribed")\n }\n func onUnsubscribed(_ s: CentrifugeSubscription, _ e: CentrifugeUnsubscribedEvent) {\n print("unsubscribed", e.code, e.reason)\n }\n}\n\ndo {\n sub = try self.client?.newSubscription(channel: "example", delegate: SubscriptionDelegate())\n sub!.subscribe()\n} catch {\n print("Can not create subscription: \\(error)")\n}\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'SubscriptionEventListener subListener = new SubscriptionEventListener() {\n @Override\n public void onSubscribed(Subscription sub, SubscribedEvent event) {\n System.out.println("subscribed to " + sub.getChannel());\n }\n @Override\n public void onSubscribing(Subscription sub, SubscribingEvent event) {\n System.out.printf("subscribing " + sub.getChannel());\n }\n @Override\n public void onUnsubscribed(Subscription sub, UnsubscribedEvent event) {\n System.out.println("unsubscribed " + sub.getChannel());\n }\n};\n\nSubscription sub;\ntry {\n sub = client.newSubscription("example", subListener);\n sub.subscribe();\n} catch (DuplicateSubscriptionException e) {\n e.printStackTrace();\n}\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'sub, err := client.NewSubscription("example")\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nsub.OnSubscribing(func(e centrifuge.SubscribingEvent) {\n\tlog.Printf("Subscribing on channel %s - %d (%s)", sub.Channel, e.Code, e.Reason)\n})\nsub.OnSubscribed(func(e centrifuge.SubscribedEvent) {\n\tlog.Printf("Subscribed on channel %s", sub.Channel)\n})\nsub.OnUnsubscribed(func(e centrifuge.UnsubscribedEvent) {\n\tlog.Printf("Unsubscribed from channel %s - %d (%s)", sub.Channel, e.Code, e.Reason)\n})\n\nerr = sub.Subscribe()\nif err != nil {\n\tlog.Fatalln(err)\n}\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Subscriptions also go to ",(0,s.jsx)(n.code,{children:"subscribing"})," state when Client connection (i.e. transport) becomes unavailable. Upon connection re-establishement all subscriptions which are not in ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state will resubscribe automatically."]}),"\n",(0,s.jsx)(n.p,{children:"In case of successful subscription states will transition like this:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"unsubscribed"})," (initial) -> ",(0,s.jsx)(n.code,{children:"subscribing"})," (",(0,s.jsx)(n.code,{children:"on('subscribing')"})," called) -> ",(0,s.jsx)(n.code,{children:"subscribed"})," (",(0,s.jsx)(n.code,{children:"on('subscribed')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of connected and subscribed Client temporary lost a connection with a server and then succesfully reconnected and resubscribed:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"subscribed"})," -> ",(0,s.jsx)(n.code,{children:"subscribing"})," (",(0,s.jsx)(n.code,{children:"on('subscribing')"})," called) -> ",(0,s.jsx)(n.code,{children:"subscribed"})," (",(0,s.jsx)(n.code,{children:"on('subscribed')"})," called)."]}),"\n",(0,s.jsxs)(n.p,{children:["Both ",(0,s.jsx)(n.code,{children:"subscribing"})," and ",(0,s.jsx)(n.code,{children:"unsubscribed"})," events have numeric ",(0,s.jsx)(n.code,{children:"code"})," and human-readable string ",(0,s.jsx)(n.code,{children:"reason"})," in their context, so you can look at them and find the exact reason why Subscription went to subscribing state or to unsubscribed state."]}),"\n",(0,s.jsx)(n.p,{children:"This diagram demonstrates possible Subscription state transitions:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifugo scheme",src:i(13305).Z+"",width:"2391",height:"1672"})}),"\n",(0,s.jsxs)(n.p,{children:["You can listen for all errors happening internally in Subscription (which are not reflected by state changes, for example, temporary subscribe errors, subscription token related errors, etc) by using ",(0,s.jsx)(n.code,{children:"error"})," event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.on('error', function(ctx) {\n console.log(\"subscription error\", ctx);\n});\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to unsubscribe from a channel call ",(0,s.jsx)(n.code,{children:".unsubscribe()"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.unsubscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case ",(0,s.jsx)(n.code,{children:"on('unsubscribed')"})," will be called. Subscription still kept in Client's registry, but no resubscription attempts will be made. You can call ",(0,s.jsx)(n.code,{children:"subscribe()"})," again when you need Subscription again. Or you can remove Subscription from Client's registry (see below)."]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-management",children:"Subscription management"}),"\n",(0,s.jsx)(n.p,{children:"The client SDK provides several methods to manage the internal registry of client-side subscriptions."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"newSubscription(channel, options)"})," allocates a new Subscription in the registry or throws an exception if the Subscription is already there. We will discuss common Subscription options below."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"getSubscription(channel)"})," returns the existing Subscription by a channel from the registry (or null if it does not exist)."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"removeSubscription(sub)"})," removes Subscription from Client's registry. Subscription is automatically unsubscribed before being removed. Use this to free resources if you don't need a Subscription to a channel anymore."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"subscriptions()"})," returns all registered subscriptions, so you can iterate over all and do some action if required (for example, you want to unsubscribe/remove all subscriptions)."]}),"\n",(0,s.jsx)(n.h2,{id:"listen-to-channel-publications",children:"Listen to channel publications"}),"\n",(0,s.jsx)(n.p,{children:"Of course the main point of having Subscriptions is the ability to listen for publications (i.e. messages published to a channel)."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.on('publication', function(ctx) {\n console.log(\"received publication\", ctx);\n});\n"})}),"\n",(0,s.jsx)(n.p,{children:"Publication context has several fields:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"data"})," - publication payload, this can be JSON or binary data"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"offset"})," - optional offset inside history stream, this is an incremental number"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"tags"})," - optional tags, this is a map with string keys and string values"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"info"})," - optional information about client connection who published this (only exists if publication comes from client-side ",(0,s.jsx)(n.code,{children:"publish()"})," API)."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["So minimal code where we connect to a server and listen for messages published into ",(0,s.jsx)(n.code,{children:"example"})," channel may look like:"]}),"\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = client.newSubscription('example').on('publication', function(ctx) {\n console.log(\"received publication from a channel\", ctx.data);\n});\n\nsub.subscribe();\n\nclient.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final client = centrifuge.createClient(\n 'ws://localhost:8000/connection/websocket',\n centrifuge.ClientConfig(),\n);\n\nfinal subscription = client.newSubscription(channel);\nsubscription.publication.listen((event) {\n print(event);\n});\nawait subscription.subscribe();\n\nawait client.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'import SwiftCentrifuge\n\nclass ClientDelegate : NSObject, CentrifugeClientDelegate {}\n\nlet config = CentrifugeClientConfig()\nlet endpoint = "ws://localhost:8000/connection/websocket"\nlet client = CentrifugeClient(endpoint: endpoint, config: config, delegate: ClientDelegate())\n\nclass SubscriptionDelegate : NSObject, CentrifugeSubscriptionDelegate {\n func onPublication(_ s: CentrifugeSubscription, _ e: CentrifugePublicationEvent) {\n print("publication", e.data)\n }\n}\n\ndo {\n sub = try self.client?.newSubscription(channel: "example", delegate: SubscriptionDelegate())\n sub!.subscribe()\n} catch {\n print("Can not create subscription: \\(error)")\n}\n\nclient.connect()\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'EventListener listener = new EventListener() {};\nOptions opts = new Options();\nClient client = new Client("ws://localhost:8000/connection/websocket", opts, listener);\n\nSubscriptionEventListener subListener = new SubscriptionEventListener() {\n @Override\n public void onPublication(Subscription sub, PublicationEvent event) {\n System.out.println("publication from " + sub.getChannel());\n }\n};\n\nSubscription sub;\ntry {\n sub = client.newSubscription("example", subListener);\n sub.subscribe();\n} catch (DuplicateSubscriptionException e) {\n e.printStackTrace();\n}\n\nclient.connect();\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'client := centrifuge.NewJsonClient(\n "ws://localhost:8000/connection/websocket",\n centrifuge.Config{},\n)\n// defer client.Close()\n\nsub, err := client.NewSubscription("example")\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nsub.OnPublication(func(e centrifuge.PublicationEvent) {\n\tlog.Printf("Publication from channel")\n})\n\nerr = sub.Subscribe()\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nif err = client.connect(); err != nil {\n log.Fatalln(err)\n}\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Note, that we can call ",(0,s.jsx)(n.code,{children:"subscribe()"})," before making a connection to a server \u2013 and this will work just fine, subscription goes to ",(0,s.jsx)(n.code,{children:"subscribing"})," state and will be subscribed upon succesfull connection. And of course, it's possible to call ",(0,s.jsx)(n.code,{children:".subscribe()"})," after ",(0,s.jsx)(n.code,{children:".connect()"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-recovery-state",children:"Subscription recovery state"}),"\n",(0,s.jsx)(n.p,{children:"Subscriptions to channels with recovery option enabled maintain stream position information internally. On every publication received this information updated and used to recover missed publications upon resubscribe (caused by reconnect for example)."}),"\n",(0,s.jsxs)(n.p,{children:["When you call ",(0,s.jsx)(n.code,{children:"unsubscribe()"})," Subscription position state is not cleared. So it's possible to call ",(0,s.jsx)(n.code,{children:"subscribe()"})," later and catch up a state."]}),"\n",(0,s.jsxs)(n.p,{children:["The recovery process result \u2013 i.e. whether all missed publications recovered or not \u2013 can be found in ",(0,s.jsx)(n.code,{children:"on('subscribed')"})," event context. Centrifuge protocol provides two fields:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"wasRecovering"})," - boolean flag that tells whether recovery was used during subscription process resulted into subscribed state. Can be useful if you want to distinguish first subscribe attempt (when subscription does not have any position information yet)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"recovered"})," - boolean flag that tells whether Centrifugo thinks that all missed publications can be successfully recovered and there is no need to load state from the main application database. It's always ",(0,s.jsx)(n.code,{children:"false"})," when ",(0,s.jsx)(n.code,{children:"wasRecovering"})," is ",(0,s.jsx)(n.code,{children:"false"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-common-options",children:"Subscription common options"}),"\n",(0,s.jsx)(n.p,{children:"There are several common options available when creating Subscription instance."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["option to set subscription token and callback to get subscription token upon expiration (see ",(0,s.jsx)(n.a,{href:"#subscription-token",children:"below more details"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:["option to set subscription ",(0,s.jsx)(n.code,{children:"data"})," (attached to every subscribe/resubscribe request)"]}),"\n",(0,s.jsx)(n.li,{children:"options to tweak resubscribe backoff algorithm"}),"\n",(0,s.jsxs)(n.li,{children:["option to start Subscription ",(0,s.jsx)(n.code,{children:"since"})," known Stream Position (i.e. attempt recovery on first subscribe)"]}),"\n",(0,s.jsxs)(n.li,{children:["option to ask server to make subscription ",(0,s.jsx)(n.code,{children:"positioned"})," (if not forced by a server)"]}),"\n",(0,s.jsxs)(n.li,{children:["option to ask server to make subscription ",(0,s.jsx)(n.code,{children:"recoverable"})," (if not forced by a server)"]}),"\n",(0,s.jsx)(n.li,{children:"option to ask server to push Join/Leave messages (if not forced by a server)"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-methods",children:"Subscription methods"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"subscribe()"})," \u2013 start subscribing to a channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"unsubscribe()"})," - unsubscribe from a channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"publish(data)"})," - publish data to Subscription channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"history(options)"})," - request Subscription channel history"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"presence()"})," - request Subscription channel online presence information"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"presenceStats()"})," - request Subscription channel online presence stats information (number of client connections and unique users in a channel)."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-token",children:"Subscription token"}),"\n",(0,s.jsx)(n.p,{children:"All SDKs support subscribing to Centrifugo channels with JWT. Channel subscription token can be set as a Subscription option upon initialization. Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE'\n});\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.p,{children:"If token sets subscription expiration client SDK will keep token refreshed. It does this by calling special callback function. This callback must return a new token. If new token with updated subscription expiration returned from a calbback then it's sent to Centrifugo. If your callback returns an empty string \u2013 this means user has no permission to subscribe to a channel anymore and subscription will be unsubscribed. In case of error returned by your callback SDK will retry operation after some jittered time."}),"\n",(0,s.jsx)(n.p,{children:"An example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"function getToken(url, ctx) {\n return new Promise((resolve, reject) => {\n fetch(url, {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify(ctx)\n })\n .then(res => {\n if (!res.ok) {\n throw new Error(`Unexpected status code ${res.status}`);\n }\n return res.json();\n })\n .then(data => {\n resolve(data.token);\n })\n .catch(err => {\n reject(err);\n });\n });\n}\n\nconst client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE',\n getToken: function (ctx) {\n // ctx has channel in the Subscription token case.\n return getToken('/centrifuge/subscription_token', ctx);\n },\n});\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["If initial token is not provided, but ",(0,s.jsx)(n.code,{children:"getToken"})," is specified \u2013 then SDK should assume that developer wants to use token authorization for a channel subscription. In this case SDK should attempt to get a subscription token before initial subscribe."]})}),"\n",(0,s.jsx)(n.h2,{id:"server-side-subscriptions",children:"Server-side subscriptions"}),"\n",(0,s.jsx)(n.p,{children:"We encourage using client-side subscriptions where possible as they provide a better control and isolation from connection. But in some cases you may want to use server-side subscriptions (i.e. subscriptions created by server upon connection establishment)."}),"\n",(0,s.jsx)(n.p,{children:"Technically, client SDK keeps server-side subscriptions in internal registry (similar to client-side subscriptions but without possibility to control them)."}),"\n",(0,s.jsx)(n.p,{children:"To listen for server-side subscription events use callbacks as shown in example below:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nclient.on('subscribed', function(ctx) {\n // Called when subscribed to a server-side channel upon Client moving to\n // connected state or during connection lifetime if server sends Subscribe\n // push message.\n console.log('subscribed to server-side channel', ctx.channel);\n});\n\nclient.on('subscribing', function(ctx) {\n // Called when existing connection lost (Client reconnects) or Client\n // explicitly disconnected. Client continue keeping server-side subscription\n // registry with stream position information where applicable.\n console.log('subscribing to server-side channel', ctx.channel);\n});\n\nclient.on('unsubscribed', function(ctx) {\n // Called when server sent unsubscribe push or server-side subscription\n // previously existed in SDK registry disappeared upon Client reconnect.\n console.log('unsubscribed from server-side channel', ctx.channel);\n});\n\nclient.on('publication', function(ctx) {\n // Called when server sends Publication over server-side subscription.\n console.log('publication receive from server-side channel', ctx.channel, ctx.data);\n});\n\nclient.connect();\n"})}),"\n",(0,s.jsx)(n.p,{children:"Server-side subscription events mostly mimic events of client-side subscriptions. But again \u2013 they do not provide control to the client and managed entirely by a server side."}),"\n",(0,s.jsx)(n.p,{children:"Additionally, Client has several top-level methods to call with server-side subscription related operations:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"publish(channel, data)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"history(channel, options)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"presence(channel)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"presenceStats(channel)"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"error-codes",children:"Error codes"}),"\n",(0,s.jsx)(n.p,{children:"Server can return error codes in range 100-1999. Error codes in interval 0-399 reserved by Centrifuge/Centrifugo server. Codes in range [400, 1999] may be returned by application code built on top of Centrifuge/Centrifugo."}),"\n",(0,s.jsxs)(n.p,{children:["Server errors contain a ",(0,s.jsx)(n.code,{children:"temporary"})," boolean flag which works as a signal that error may be fixed by a later retry."]}),"\n",(0,s.jsx)(n.p,{children:"Errors with codes 0-100 can be used by client-side implementation. Client-side errors may not have code attached at all since in many languages error can be distinguished by its type."}),"\n",(0,s.jsx)(n.h2,{id:"unsubscribe-codes",children:"Unsubscribe codes"}),"\n",(0,s.jsxs)(n.p,{children:["Server may return unsubscribe codes. Server unsubscribe codes must be in range ",(0,s.jsx)(n.code,{children:"[2000, 2999]"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Unsubscribe codes >= 2500 coming from server to client result into automatic resubscribe attempt (i.e. client goes to ",(0,s.jsx)(n.code,{children:"subscribing"})," state). Codes < 2500 result into going to ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"Client implementation can use codes < 2000 for client-side specific unsubscribe reasons."}),"\n",(0,s.jsx)(n.h2,{id:"disconnect-codes",children:"Disconnect codes"}),"\n",(0,s.jsxs)(n.p,{children:["Server may send custom disconnect codes to a client. Custom disconnect codes must be in range ",(0,s.jsx)(n.code,{children:"[3000, 4999]"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Client automatically reconnects upon receiving code in range 3000-3499, 4000-4499 (i.e. Client goes to ",(0,s.jsx)(n.code,{children:"connecting"})," state). Other codes result into going to ",(0,s.jsx)(n.code,{children:"disconnected"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"Client implementation can use codes < 3000 for client-side specific disconnect reasons."}),"\n",(0,s.jsx)(n.h2,{id:"rpc",children:"RPC"}),"\n",(0,s.jsxs)(n.p,{children:["An SDK provides a way to send RPC to a server. RPC is a call that is not related to channels at all. It's just a way to call the server method from the client-side over the real-time connection. RPC is only available when ",(0,s.jsx)(n.a,{href:"/docs/4/server/proxy#rpc-proxy",children:"RPC proxy"})," configured (so Centrifugo proxies the RPC to your application backend)."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const rpcRequest = {'key': 'value'};\nconst data = await centrifuge.namedRPC('example_method', rpcRequest);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"channel-history-api",children:"Channel history API"}),"\n",(0,s.jsx)(n.p,{children:"SDK provides a method to call publication history inside a channel (only for channels where history is enabled) to get last publications in a channel."}),"\n",(0,s.jsx)(n.p,{children:"Get stream current top position:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history();\nconsole.log(resp.offset);\nconsole.log(resp.epoch);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since known stream position:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10, since: {offset: 0, epoch: '...'}});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since current stream beginning:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since current stream end in reversed order (last to first):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10, reverse: true});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"presence-and-presence-stats-api",children:"Presence and presence stats API"}),"\n",(0,s.jsxs)(n.p,{children:["Once subscribed client can call presence and presence stats information inside channel (only for channels where ",(0,s.jsx)(n.a,{href:"/docs/4/server/channels#channel-options",children:"presence configured"}),"):"]}),"\n",(0,s.jsx)(n.p,{children:"For presence (full information about active subscribers in channel):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presence();\n// resp contains presence information - a map client IDs as keys \n// and client information as values.\n"})}),"\n",(0,s.jsx)(n.p,{children:"For presence stats (just a number of clients and unique users in a channel):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presenceStats();\n// resp contains a number of clients and a number of unique users.\n"})}),"\n",(0,s.jsx)(n.h2,{id:"sdk-common-best-practices",children:"SDK common best practices"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Callbacks must be fast. Avoid blocking operations inside event handlers. Callbacks caused by protocol messages received from a server are called synchronously and connection read loop is blocked while such callbacks are being executed. Consider doing heavy work asynchronously."}),"\n",(0,s.jsx)(n.li,{children:"Do not blindly rely on the current Client or Subscription state when making client API calls \u2013 state can change at any moment, so don't forget to handle errors."}),"\n",(0,s.jsx)(n.li,{children:"Disconnect from a server when a mobile application goes to the background since a mobile OS can kill the connection at some point without any callbacks called."}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},77262:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/client_state-34264b7a7eee2792baa58bb5bb525d46.png"},13305:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/sub_state-9dbaf6d2a6868264a330b1a3f4c59b39.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>r,a:()=>o});var s=i(67294);const t={},c=s.createContext(t);function o(e){const n=s.useContext(c);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2f70c421.f5b43c19.js b/assets/js/2f70c421.f5b43c19.js new file mode 100644 index 000000000..e4a36029d --- /dev/null +++ b/assets/js/2f70c421.f5b43c19.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4633],{75282:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>r,metadata:()=>a,toc:()=>u});var s=i(85893),t=i(11151),c=i(74866),o=i(85162);const r={id:"client_api",title:"Client SDK API"},l=void 0,a={id:"transports/client_api",title:"Client SDK API",description:"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.",source:"@site/versioned_docs/version-4/transports/client_api.md",sourceDirName:"transports",slug:"/transports/client_api",permalink:"/docs/4/transports/client_api",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/transports/client_api.md",tags:[],version:"4",frontMatter:{id:"client_api",title:"Client SDK API"},sidebar:"Transports",previous:{title:"Real-time transports",permalink:"/docs/4/transports/overview"},next:{title:"Client real-time SDKs",permalink:"/docs/4/transports/client_sdk"}},d={},u=[{value:"Client connection states",id:"client-connection-states",level:2},{value:"Client common options",id:"client-common-options",level:2},{value:"Client methods",id:"client-methods",level:2},{value:"Client connection token",id:"client-connection-token",level:2},{value:"Connection PING/PONG",id:"connection-pingpong",level:2},{value:"Subscription states",id:"subscription-states",level:2},{value:"Subscription management",id:"subscription-management",level:2},{value:"Listen to channel publications",id:"listen-to-channel-publications",level:2},{value:"Subscription recovery state",id:"subscription-recovery-state",level:2},{value:"Subscription common options",id:"subscription-common-options",level:2},{value:"Subscription methods",id:"subscription-methods",level:2},{value:"Subscription token",id:"subscription-token",level:2},{value:"Server-side subscriptions",id:"server-side-subscriptions",level:2},{value:"Error codes",id:"error-codes",level:2},{value:"Unsubscribe codes",id:"unsubscribe-codes",level:2},{value:"Disconnect codes",id:"disconnect-codes",level:2},{value:"RPC",id:"rpc",level:2},{value:"Channel history API",id:"channel-history-api",level:2},{value:"Presence and presence stats API",id:"presence-and-presence-stats-api",level:2},{value:"SDK common best practices",id:"sdk-common-best-practices",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/protocol/blob/master/definitions/client.proto",children:"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."]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"For Centrifugo v4 we introduced a new generation of SDKs for Javascript, Dart, Go, Swift, and Java \u2013 all based on updated client protocol and new client API iteration."})}),"\n",(0,s.jsxs)(n.p,{children:["This chapter describes the core concepts of client SDKs API. If you want to find out lower-level client protocol framing details then look at ",(0,s.jsx)(n.a,{href:"/docs/4/transports/client_protocol",children:"client protocol"})," document."]}),"\n",(0,s.jsxs)(n.p,{children:["Most examples here are written using our Javascript client (",(0,s.jsx)(n.code,{children:"centrifuge-js"}),"), but all other Centrifugo connectors now have very similar semantics and APIs very close to each other."]}),"\n",(0,s.jsx)(n.h2,{id:"client-connection-states",children:"Client connection states"}),"\n",(0,s.jsx)(n.p,{children:"Client connection has 4 states:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"disconnected"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"connecting"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"connected"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"closed"})}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"closed"})," state is only implemented by SDKs where it makes sense (need to clean up allocated resources when app gracefully shuts down \u2013 for example in Java SDK we close thread executors used internally)."]})}),"\n",(0,s.jsxs)(n.p,{children:["When a new Client is created it has a ",(0,s.jsx)(n.code,{children:"disconnected"})," state. To connect to a server ",(0,s.jsx)(n.code,{children:"connect()"})," method must be called. After calling connect Client moves to the ",(0,s.jsx)(n.code,{children:"connecting"})," state. If a Client can't connect to a server it attempts to create a connection with an exponential backoff algorithm (with ",(0,s.jsx)(n.a,{href:"https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/",children:"full jitter"}),"). If a connection to a server is successful then the state becomes ",(0,s.jsx)(n.code,{children:"connected"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["If a connection is lost (due to a missing network for example, or due to reconnect advice received from a server, or due to some client-side error that can't be recovered without reconnecting) Client goes to the ",(0,s.jsx)(n.code,{children:"connecting"})," state again. In this state Client tries to reconnect (again, with an exponential backoff algorithm)."]}),"\n",(0,s.jsxs)(n.p,{children:["The Client's state can become ",(0,s.jsx)(n.code,{children:"disconnected"}),". This happens when Client's ",(0,s.jsx)(n.code,{children:"disconnect()"})," method was called by a developer. Also, this can happen due to server advice from a server, or due to a terminal problem that happened on the client-side."]}),"\n",(0,s.jsx)(n.p,{children:"Here is a program where we create a Client instance, set callbacks to listen to state updates and establish a connection with a server:"}),"\n","\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nclient.on('connecting', function(ctx) {\n console.log('connecting', ctx);\n});\n\nclient.on('connected', function(ctx) {\n console.log('connected', ctx);\n});\n\nclient.on('disconnected', function(ctx) {\n console.log('disconnected', ctx);\n});\n\nclient.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final onEvent = (dynamic event) {\n print('client event> $event');\n};\n\nfinal client = centrifuge.createClient(\n 'ws://localhost:8000/connection/websocket',\n centrifuge.ClientConfig(),\n);\n\nclient.connecting.listen(onEvent);\nclient.connected.listen(onEvent);\nclient.disconnected.listen(onEvent);\n\nawait client.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'import SwiftCentrifuge\n\nclass ClientDelegate : NSObject, CentrifugeClientDelegate {\n func onConnecting(_ c: CentrifugeClient, _ e: CentrifugeConnectingEvent) {\n print("connecting", e.code, e.reason)\n }\n func onConnected(_ client: CentrifugeClient, _ e: CentrifugeConnectedEvent) {\n print("connected with id", e.client)\n }\n func onDisconnected(_ client: CentrifugeClient, _ e: CentrifugeDisconnectedEvent) {\n print("disconnected", e.code, e.reason)\n }\n}\n\nlet config = CentrifugeClientConfig()\nlet endpoint = "ws://localhost:8000/connection/websocket"\nlet client = CentrifugeClient(endpoint: endpoint, config: config, delegate: ClientDelegate())\nclient.connect()\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'EventListener listener = new EventListener() {\n @Override\n public void onConnected(Client client, ConnectedEvent event) {\n System.out.println("connected");\n }\n @Override\n public void onConnecting(Client client, ConnectingEvent event) {\n System.out.printf("connecting: %s%n", event.getReason());\n }\n @Override\n public void onDisconnected(Client client, DisconnectedEvent event) {\n System.out.printf("disconnected %d %s", event.getCode(), event.getReason());\n }\n};\n\nOptions opts = new Options();\n\nClient client = new Client("ws://localhost:8000/connection/websocket", opts, listener);\n\nclient.connect();\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'client := centrifuge.NewJsonClient(\n "ws://localhost:8000/connection/websocket",\n centrifuge.Config{},\n)\ndefer client.Close()\n\nclient.OnConnecting(func(e centrifuge.ConnectingEvent) {\n log.Printf("Connecting - %d (%s)", e.Code, e.Reason)\n})\nclient.OnConnected(func(e centrifuge.ConnectedEvent) {\n log.Printf("Connected with ID %s", e.ClientID)\n})\nclient.OnDisconnected(func(e centrifuge.DisconnectedEvent) {\n log.Printf("Disconnected: %d (%s)", e.Code, e.Reason)\n})\n\n_ = client.connect()\n'})})})]}),"\n",(0,s.jsx)(n.p,{children:"In case of successful connection Client states will transition like this:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"disconnected"})," (initial) -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"connected"})," (",(0,s.jsx)(n.code,{children:"on('connected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client temporary lost a connection with a server and then successfully reconnected:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"connected"})," (",(0,s.jsx)(n.code,{children:"on('connected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client temporary lost a connection with a server, but got a terminal error upon reconnection:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"connecting"})," (",(0,s.jsx)(n.code,{children:"on('connecting')"})," called) -> ",(0,s.jsx)(n.code,{children:"disconnected"})," (",(0,s.jsx)(n.code,{children:"on('disconnected')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of already connected Client came across terminal condition (for example, if during a connection token refresh application found that user has no permission to connect anymore):"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"connected"})," -> ",(0,s.jsx)(n.code,{children:"disconnected"})," (",(0,s.jsx)(n.code,{children:"on('disconnected')"})," called)."]}),"\n",(0,s.jsxs)(n.p,{children:["Both ",(0,s.jsx)(n.code,{children:"connecting"})," and ",(0,s.jsx)(n.code,{children:"disconnected"})," events have numeric ",(0,s.jsx)(n.code,{children:"code"})," and human-readable string ",(0,s.jsx)(n.code,{children:"reason"})," in their context, so you can look at them and find the exact reason why the Client went to the ",(0,s.jsx)(n.code,{children:"connecting"})," state or to the ",(0,s.jsx)(n.code,{children:"disconnected"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"This diagram demonstrates possible Client state transitions:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifugo scheme",src:i(52869).Z+"",width:"2352",height:"1700"})}),"\n",(0,s.jsxs)(n.p,{children:["You can also listen for all errors happening internally (which are not reflected by state changes, for example, transport errors happening on initial connect, transport during reconnect, connection token refresh related errors, etc) while the client works by using ",(0,s.jsx)(n.code,{children:"error"})," event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"client.on('error', function(ctx) {\n console.log('client error', ctx);\n});\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to disconnect from a server call ",(0,s.jsx)(n.code,{children:".disconnect()"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"client.disconnect();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case ",(0,s.jsx)(n.code,{children:"on('disconnected')"})," will be called. You can call ",(0,s.jsx)(n.code,{children:"connect()"})," again when you need to establish a connection."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"closed"})," state implemented in SDKs where resources like internal queues, thread executors, etc must be cleaned up when the Client is not needed anymore. All subscriptions should automatically go to the ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state upon closing. The client is not usable after going to a ",(0,s.jsx)(n.code,{children:"closed"})," state."]}),"\n",(0,s.jsx)(n.h2,{id:"client-common-options",children:"Client common options"}),"\n",(0,s.jsx)(n.p,{children:"There are several common options available when creating Client instance."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["option to set connection token and callback to get connection token upon expiration (see below ",(0,s.jsx)(n.a,{href:"#client-connection-token",children:"mode details"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"option to set connect data"}),"\n",(0,s.jsx)(n.li,{children:"option to configure operation timeout"}),"\n",(0,s.jsx)(n.li,{children:"tweaks for reconnect backoff algorithm (min delay, max delay)"}),"\n",(0,s.jsx)(n.li,{children:"configure max delay of server pings (to detect broken connection)"}),"\n",(0,s.jsxs)(n.li,{children:["configure headers to send in WebSocket upgrade request (except ",(0,s.jsx)(n.code,{children:"centrifuge-js"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"configure client name and version for analytics purpose"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"client-methods",children:"Client methods"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"connect()"})," \u2013 connect to a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"disconnect()"})," - disconnect from a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"close()"})," - close Client if not needed anymore"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"send(data)"})," - send asynchronous message to a server"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rpc(method, data)"})," - send arbitrary RPC and wait for response"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"client-connection-token",children:"Client connection token"}),"\n",(0,s.jsx)(n.p,{children:"All SDKs support connecting to Centrifugo with JWT. Initial connection token can be set in Client option upon initialization. Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE'\n});\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the token sets connection expiration then the client SDK will keep the token refreshed. It does this by calling a special callback function. This callback must return a new token. If a new token with updated connection expiration is returned from callback then it's sent to Centrifugo. If your callback returns an empty string \u2013 this means the user has no permission to connect to Centrifugo and the Client will move to a disconnected state. In case of error returned by your callback SDK will retry the operation after some jittered time."}),"\n",(0,s.jsx)(n.p,{children:"An example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"function getToken(url, ctx) {\n return new Promise((resolve, reject) => {\n fetch(url, {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify(ctx)\n })\n .then(res => {\n if (!res.ok) {\n throw new Error(`Unexpected status code ${res.status}`);\n }\n return res.json();\n })\n .then(data => {\n resolve(data.token);\n })\n .catch(err => {\n reject(err);\n });\n });\n}\n\nconst client = new Centrifuge(\n 'ws://localhost:8000/connection/websocket',\n {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE',\n getToken: function (ctx) {\n return getToken('/centrifuge/connection_token', ctx);\n }\n }\n);\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["If initial token is not provided, but ",(0,s.jsx)(n.code,{children:"getToken"})," is specified \u2013 then SDK should assume that developer wants to use token authentication. In this case SDK should attempt to get a connection token before establishing an initial connection."]})}),"\n",(0,s.jsx)(n.h2,{id:"connection-pingpong",children:"Connection PING/PONG"}),"\n",(0,s.jsx)(n.p,{children:"PINGs sent by a server, a client should answer with PONGs upon receiving PING. If a client does not receive PING from a server for a long time (ping interval + configured delay) \u2013 the connection is considered broken and will be re-established."}),"\n",(0,s.jsx)(n.h2,{id:"subscription-states",children:"Subscription states"}),"\n",(0,s.jsxs)(n.p,{children:["Client allows subscribing on channels. This can be done by creating ",(0,s.jsx)(n.code,{children:"Subscription"})," object."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription(channel);\nsub.subscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["When a",(0,s.jsx)(n.code,{children:"newSubscription"})," method is called Client allocates a new Subscription instance and saves it in the internal subscription registry. Having a registry of allocated subscriptions allows SDK to manage resubscribes upon reconnecting to a server. Centrifugo connectors do not allow creating two subscriptions to the same channel \u2013 in this case, ",(0,s.jsx)(n.code,{children:"newSubscription"})," can throw an exception."]}),"\n",(0,s.jsx)(n.p,{children:"Subscription has 3 states:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"unsubscribed"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"subscribing"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"subscribed"})}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["When a new Subscription is created it has an ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state."]}),"\n",(0,s.jsxs)(n.p,{children:["To initiate the actual process of subscribing to a channel ",(0,s.jsx)(n.code,{children:"subscribe()"})," method of Subscription instance should be called. After calling ",(0,s.jsx)(n.code,{children:"subscribe()"})," Subscription moves to ",(0,s.jsx)(n.code,{children:"subscribing"})," state."]}),"\n",(0,s.jsxs)(n.p,{children:["If subscription to a channel is not successful then depending on error type subscription can automatically resubscribe (with exponential backoff) or go to an ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state (upon non-temporary error). If subscription to a channel is successful then the state becomes ",(0,s.jsx)(n.code,{children:"subscribed"}),"."]}),"\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = client.newSubscription(channel);\n\nsub.on('subscribing', function(ctx) {\n console.log('subscribing');\n});\n\nsub.on('subscribed', function(ctx) {\n console.log('subscribed');\n});\n\nsub.on('unsubscribed', function(ctx) {\n console.log('unsubscribed');\n});\n\nsub.subscribe();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final onSubscriptionEvent = (dynamic event) async {\n print('subscription $channel> $event');\n};\n\nfinal subscription = client.newSubscription(channel);\n\nsubscription.subscribing.listen(onSubscriptionEvent);\nsubscription.subscribed.listen(onSubscriptionEvent);\nsubscription.unsubscribed.listen(onSubscriptionEvent);\n\nawait subscription.subscribe();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'class SubscriptionDelegate : NSObject, CentrifugeSubscriptionDelegate {\n func onSubscribing(_ s: CentrifugeSubscription, _ e: CentrifugeSubscribingEvent) {\n print("subscribing", e.code, e.reason)\n }\n func onSubscribed(_ s: CentrifugeSubscription, _ e: CentrifugeSubscribedEvent) {\n print("subscribed")\n }\n func onUnsubscribed(_ s: CentrifugeSubscription, _ e: CentrifugeUnsubscribedEvent) {\n print("unsubscribed", e.code, e.reason)\n }\n}\n\ndo {\n sub = try self.client?.newSubscription(channel: "example", delegate: SubscriptionDelegate())\n sub!.subscribe()\n} catch {\n print("Can not create subscription: \\(error)")\n}\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'SubscriptionEventListener subListener = new SubscriptionEventListener() {\n @Override\n public void onSubscribed(Subscription sub, SubscribedEvent event) {\n System.out.println("subscribed to " + sub.getChannel());\n }\n @Override\n public void onSubscribing(Subscription sub, SubscribingEvent event) {\n System.out.printf("subscribing " + sub.getChannel());\n }\n @Override\n public void onUnsubscribed(Subscription sub, UnsubscribedEvent event) {\n System.out.println("unsubscribed " + sub.getChannel());\n }\n};\n\nSubscription sub;\ntry {\n sub = client.newSubscription("example", subListener);\n sub.subscribe();\n} catch (DuplicateSubscriptionException e) {\n e.printStackTrace();\n}\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'sub, err := client.NewSubscription("example")\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nsub.OnSubscribing(func(e centrifuge.SubscribingEvent) {\n\tlog.Printf("Subscribing on channel %s - %d (%s)", sub.Channel, e.Code, e.Reason)\n})\nsub.OnSubscribed(func(e centrifuge.SubscribedEvent) {\n\tlog.Printf("Subscribed on channel %s", sub.Channel)\n})\nsub.OnUnsubscribed(func(e centrifuge.UnsubscribedEvent) {\n\tlog.Printf("Unsubscribed from channel %s - %d (%s)", sub.Channel, e.Code, e.Reason)\n})\n\nerr = sub.Subscribe()\nif err != nil {\n\tlog.Fatalln(err)\n}\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Subscriptions also go to ",(0,s.jsx)(n.code,{children:"subscribing"})," state when Client connection (i.e. transport) becomes unavailable. Upon connection re-establishement all subscriptions which are not in ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state will resubscribe automatically."]}),"\n",(0,s.jsx)(n.p,{children:"In case of successful subscription states will transition like this:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"unsubscribed"})," (initial) -> ",(0,s.jsx)(n.code,{children:"subscribing"})," (",(0,s.jsx)(n.code,{children:"on('subscribing')"})," called) -> ",(0,s.jsx)(n.code,{children:"subscribed"})," (",(0,s.jsx)(n.code,{children:"on('subscribed')"})," called)."]}),"\n",(0,s.jsx)(n.p,{children:"In case of connected and subscribed Client temporary lost a connection with a server and then succesfully reconnected and resubscribed:"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"subscribed"})," -> ",(0,s.jsx)(n.code,{children:"subscribing"})," (",(0,s.jsx)(n.code,{children:"on('subscribing')"})," called) -> ",(0,s.jsx)(n.code,{children:"subscribed"})," (",(0,s.jsx)(n.code,{children:"on('subscribed')"})," called)."]}),"\n",(0,s.jsxs)(n.p,{children:["Both ",(0,s.jsx)(n.code,{children:"subscribing"})," and ",(0,s.jsx)(n.code,{children:"unsubscribed"})," events have numeric ",(0,s.jsx)(n.code,{children:"code"})," and human-readable string ",(0,s.jsx)(n.code,{children:"reason"})," in their context, so you can look at them and find the exact reason why Subscription went to subscribing state or to unsubscribed state."]}),"\n",(0,s.jsx)(n.p,{children:"This diagram demonstrates possible Subscription state transitions:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"Centrifugo scheme",src:i(48720).Z+"",width:"2391",height:"1672"})}),"\n",(0,s.jsxs)(n.p,{children:["You can listen for all errors happening internally in Subscription (which are not reflected by state changes, for example, temporary subscribe errors, subscription token related errors, etc) by using ",(0,s.jsx)(n.code,{children:"error"})," event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.on('error', function(ctx) {\n console.log(\"subscription error\", ctx);\n});\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If you want to unsubscribe from a channel call ",(0,s.jsx)(n.code,{children:".unsubscribe()"})," method:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.unsubscribe();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In this case ",(0,s.jsx)(n.code,{children:"on('unsubscribed')"})," will be called. Subscription still kept in Client's registry, but no resubscription attempts will be made. You can call ",(0,s.jsx)(n.code,{children:"subscribe()"})," again when you need Subscription again. Or you can remove Subscription from Client's registry (see below)."]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-management",children:"Subscription management"}),"\n",(0,s.jsx)(n.p,{children:"The client SDK provides several methods to manage the internal registry of client-side subscriptions."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"newSubscription(channel, options)"})," allocates a new Subscription in the registry or throws an exception if the Subscription is already there. We will discuss common Subscription options below."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"getSubscription(channel)"})," returns the existing Subscription by a channel from the registry (or null if it does not exist)."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"removeSubscription(sub)"})," removes Subscription from Client's registry. Subscription is automatically unsubscribed before being removed. Use this to free resources if you don't need a Subscription to a channel anymore."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"subscriptions()"})," returns all registered subscriptions, so you can iterate over all and do some action if required (for example, you want to unsubscribe/remove all subscriptions)."]}),"\n",(0,s.jsx)(n.h2,{id:"listen-to-channel-publications",children:"Listen to channel publications"}),"\n",(0,s.jsx)(n.p,{children:"Of course the main point of having Subscriptions is the ability to listen for publications (i.e. messages published to a channel)."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"sub.on('publication', function(ctx) {\n console.log(\"received publication\", ctx);\n});\n"})}),"\n",(0,s.jsx)(n.p,{children:"Publication context has several fields:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"data"})," - publication payload, this can be JSON or binary data"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"offset"})," - optional offset inside history stream, this is an incremental number"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"tags"})," - optional tags, this is a map with string keys and string values"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"info"})," - optional information about client connection who published this (only exists if publication comes from client-side ",(0,s.jsx)(n.code,{children:"publish()"})," API)."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["So minimal code where we connect to a server and listen for messages published into ",(0,s.jsx)(n.code,{children:"example"})," channel may look like:"]}),"\n",(0,s.jsxs)(c.Z,{className:"unique-tabs",defaultValue:"javascript",values:[{label:"Javascript",value:"javascript"},{label:"Dart",value:"dart"},{label:"Swift",value:"swift"},{label:"Java",value:"java"},{label:"Go",value:"go"}],children:[(0,s.jsx)(o.Z,{value:"javascript",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = client.newSubscription('example').on('publication', function(ctx) {\n console.log(\"received publication from a channel\", ctx.data);\n});\n\nsub.subscribe();\n\nclient.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"dart",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-dart",children:"final client = centrifuge.createClient(\n 'ws://localhost:8000/connection/websocket',\n centrifuge.ClientConfig(),\n);\n\nfinal subscription = client.newSubscription(channel);\nsubscription.publication.listen((event) {\n print(event);\n});\nawait subscription.subscribe();\n\nawait client.connect();\n"})})}),(0,s.jsx)(o.Z,{value:"swift",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",children:'import SwiftCentrifuge\n\nclass ClientDelegate : NSObject, CentrifugeClientDelegate {}\n\nlet config = CentrifugeClientConfig()\nlet endpoint = "ws://localhost:8000/connection/websocket"\nlet client = CentrifugeClient(endpoint: endpoint, config: config, delegate: ClientDelegate())\n\nclass SubscriptionDelegate : NSObject, CentrifugeSubscriptionDelegate {\n func onPublication(_ s: CentrifugeSubscription, _ e: CentrifugePublicationEvent) {\n print("publication", e.data)\n }\n}\n\ndo {\n sub = try self.client?.newSubscription(channel: "example", delegate: SubscriptionDelegate())\n sub!.subscribe()\n} catch {\n print("Can not create subscription: \\(error)")\n}\n\nclient.connect()\n'})})}),(0,s.jsx)(o.Z,{value:"java",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-java",children:'EventListener listener = new EventListener() {};\nOptions opts = new Options();\nClient client = new Client("ws://localhost:8000/connection/websocket", opts, listener);\n\nSubscriptionEventListener subListener = new SubscriptionEventListener() {\n @Override\n public void onPublication(Subscription sub, PublicationEvent event) {\n System.out.println("publication from " + sub.getChannel());\n }\n};\n\nSubscription sub;\ntry {\n sub = client.newSubscription("example", subListener);\n sub.subscribe();\n} catch (DuplicateSubscriptionException e) {\n e.printStackTrace();\n}\n\nclient.connect();\n'})})}),(0,s.jsx)(o.Z,{value:"go",children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-go",children:'client := centrifuge.NewJsonClient(\n "ws://localhost:8000/connection/websocket",\n centrifuge.Config{},\n)\n// defer client.Close()\n\nsub, err := client.NewSubscription("example")\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nsub.OnPublication(func(e centrifuge.PublicationEvent) {\n\tlog.Printf("Publication from channel")\n})\n\nerr = sub.Subscribe()\nif err != nil {\n\tlog.Fatalln(err)\n}\n\nif err = client.connect(); err != nil {\n log.Fatalln(err)\n}\n'})})})]}),"\n",(0,s.jsxs)(n.p,{children:["Note, that we can call ",(0,s.jsx)(n.code,{children:"subscribe()"})," before making a connection to a server \u2013 and this will work just fine, subscription goes to ",(0,s.jsx)(n.code,{children:"subscribing"})," state and will be subscribed upon succesfull connection. And of course, it's possible to call ",(0,s.jsx)(n.code,{children:".subscribe()"})," after ",(0,s.jsx)(n.code,{children:".connect()"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-recovery-state",children:"Subscription recovery state"}),"\n",(0,s.jsx)(n.p,{children:"Subscriptions to channels with recovery option enabled maintain stream position information internally. On every publication received this information updated and used to recover missed publications upon resubscribe (caused by reconnect for example)."}),"\n",(0,s.jsxs)(n.p,{children:["When you call ",(0,s.jsx)(n.code,{children:"unsubscribe()"})," Subscription position state is not cleared. So it's possible to call ",(0,s.jsx)(n.code,{children:"subscribe()"})," later and catch up a state."]}),"\n",(0,s.jsxs)(n.p,{children:["The recovery process result \u2013 i.e. whether all missed publications recovered or not \u2013 can be found in ",(0,s.jsx)(n.code,{children:"on('subscribed')"})," event context. Centrifuge protocol provides two fields:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"wasRecovering"})," - boolean flag that tells whether recovery was used during subscription process resulted into subscribed state. Can be useful if you want to distinguish first subscribe attempt (when subscription does not have any position information yet)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"recovered"})," - boolean flag that tells whether Centrifugo thinks that all missed publications can be successfully recovered and there is no need to load state from the main application database. It's always ",(0,s.jsx)(n.code,{children:"false"})," when ",(0,s.jsx)(n.code,{children:"wasRecovering"})," is ",(0,s.jsx)(n.code,{children:"false"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-common-options",children:"Subscription common options"}),"\n",(0,s.jsx)(n.p,{children:"There are several common options available when creating Subscription instance."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["option to set subscription token and callback to get subscription token upon expiration (see ",(0,s.jsx)(n.a,{href:"#subscription-token",children:"below more details"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:["option to set subscription ",(0,s.jsx)(n.code,{children:"data"})," (attached to every subscribe/resubscribe request)"]}),"\n",(0,s.jsx)(n.li,{children:"options to tweak resubscribe backoff algorithm"}),"\n",(0,s.jsxs)(n.li,{children:["option to start Subscription ",(0,s.jsx)(n.code,{children:"since"})," known Stream Position (i.e. attempt recovery on first subscribe)"]}),"\n",(0,s.jsxs)(n.li,{children:["option to ask server to make subscription ",(0,s.jsx)(n.code,{children:"positioned"})," (if not forced by a server)"]}),"\n",(0,s.jsxs)(n.li,{children:["option to ask server to make subscription ",(0,s.jsx)(n.code,{children:"recoverable"})," (if not forced by a server)"]}),"\n",(0,s.jsx)(n.li,{children:"option to ask server to push Join/Leave messages (if not forced by a server)"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-methods",children:"Subscription methods"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"subscribe()"})," \u2013 start subscribing to a channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"unsubscribe()"})," - unsubscribe from a channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"publish(data)"})," - publish data to Subscription channel"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"history(options)"})," - request Subscription channel history"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"presence()"})," - request Subscription channel online presence information"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"presenceStats()"})," - request Subscription channel online presence stats information (number of client connections and unique users in a channel)."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"subscription-token",children:"Subscription token"}),"\n",(0,s.jsx)(n.p,{children:"All SDKs support subscribing to Centrifugo channels with JWT. Channel subscription token can be set as a Subscription option upon initialization. Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE'\n});\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.p,{children:"If token sets subscription expiration client SDK will keep token refreshed. It does this by calling special callback function. This callback must return a new token. If new token with updated subscription expiration returned from a calbback then it's sent to Centrifugo. If your callback returns an empty string \u2013 this means user has no permission to subscribe to a channel anymore and subscription will be unsubscribed. In case of error returned by your callback SDK will retry operation after some jittered time."}),"\n",(0,s.jsx)(n.p,{children:"An example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"function getToken(url, ctx) {\n return new Promise((resolve, reject) => {\n fetch(url, {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify(ctx)\n })\n .then(res => {\n if (!res.ok) {\n throw new Error(`Unexpected status code ${res.status}`);\n }\n return res.json();\n })\n .then(data => {\n resolve(data.token);\n })\n .catch(err => {\n reject(err);\n });\n });\n}\n\nconst client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE',\n getToken: function (ctx) {\n // ctx has channel in the Subscription token case.\n return getToken('/centrifuge/subscription_token', ctx);\n },\n});\nsub.subscribe();\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["If initial token is not provided, but ",(0,s.jsx)(n.code,{children:"getToken"})," is specified \u2013 then SDK should assume that developer wants to use token authorization for a channel subscription. In this case SDK should attempt to get a subscription token before initial subscribe."]})}),"\n",(0,s.jsx)(n.h2,{id:"server-side-subscriptions",children:"Server-side subscriptions"}),"\n",(0,s.jsx)(n.p,{children:"We encourage using client-side subscriptions where possible as they provide a better control and isolation from connection. But in some cases you may want to use server-side subscriptions (i.e. subscriptions created by server upon connection establishment)."}),"\n",(0,s.jsx)(n.p,{children:"Technically, client SDK keeps server-side subscriptions in internal registry (similar to client-side subscriptions but without possibility to control them)."}),"\n",(0,s.jsx)(n.p,{children:"To listen for server-side subscription events use callbacks as shown in example below:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nclient.on('subscribed', function(ctx) {\n // Called when subscribed to a server-side channel upon Client moving to\n // connected state or during connection lifetime if server sends Subscribe\n // push message.\n console.log('subscribed to server-side channel', ctx.channel);\n});\n\nclient.on('subscribing', function(ctx) {\n // Called when existing connection lost (Client reconnects) or Client\n // explicitly disconnected. Client continue keeping server-side subscription\n // registry with stream position information where applicable.\n console.log('subscribing to server-side channel', ctx.channel);\n});\n\nclient.on('unsubscribed', function(ctx) {\n // Called when server sent unsubscribe push or server-side subscription\n // previously existed in SDK registry disappeared upon Client reconnect.\n console.log('unsubscribed from server-side channel', ctx.channel);\n});\n\nclient.on('publication', function(ctx) {\n // Called when server sends Publication over server-side subscription.\n console.log('publication receive from server-side channel', ctx.channel, ctx.data);\n});\n\nclient.connect();\n"})}),"\n",(0,s.jsx)(n.p,{children:"Server-side subscription events mostly mimic events of client-side subscriptions. But again \u2013 they do not provide control to the client and managed entirely by a server side."}),"\n",(0,s.jsx)(n.p,{children:"Additionally, Client has several top-level methods to call with server-side subscription related operations:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"publish(channel, data)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"history(channel, options)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"presence(channel)"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"presenceStats(channel)"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"error-codes",children:"Error codes"}),"\n",(0,s.jsx)(n.p,{children:"Server can return error codes in range 100-1999. Error codes in interval 0-399 reserved by Centrifuge/Centrifugo server. Codes in range [400, 1999] may be returned by application code built on top of Centrifuge/Centrifugo."}),"\n",(0,s.jsxs)(n.p,{children:["Server errors contain a ",(0,s.jsx)(n.code,{children:"temporary"})," boolean flag which works as a signal that error may be fixed by a later retry."]}),"\n",(0,s.jsx)(n.p,{children:"Errors with codes 0-100 can be used by client-side implementation. Client-side errors may not have code attached at all since in many languages error can be distinguished by its type."}),"\n",(0,s.jsx)(n.h2,{id:"unsubscribe-codes",children:"Unsubscribe codes"}),"\n",(0,s.jsxs)(n.p,{children:["Server may return unsubscribe codes. Server unsubscribe codes must be in range ",(0,s.jsx)(n.code,{children:"[2000, 2999]"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Unsubscribe codes >= 2500 coming from server to client result into automatic resubscribe attempt (i.e. client goes to ",(0,s.jsx)(n.code,{children:"subscribing"})," state). Codes < 2500 result into going to ",(0,s.jsx)(n.code,{children:"unsubscribed"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"Client implementation can use codes < 2000 for client-side specific unsubscribe reasons."}),"\n",(0,s.jsx)(n.h2,{id:"disconnect-codes",children:"Disconnect codes"}),"\n",(0,s.jsxs)(n.p,{children:["Server may send custom disconnect codes to a client. Custom disconnect codes must be in range ",(0,s.jsx)(n.code,{children:"[3000, 4999]"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["Client automatically reconnects upon receiving code in range 3000-3499, 4000-4499 (i.e. Client goes to ",(0,s.jsx)(n.code,{children:"connecting"})," state). Other codes result into going to ",(0,s.jsx)(n.code,{children:"disconnected"})," state."]}),"\n",(0,s.jsx)(n.p,{children:"Client implementation can use codes < 3000 for client-side specific disconnect reasons."}),"\n",(0,s.jsx)(n.h2,{id:"rpc",children:"RPC"}),"\n",(0,s.jsxs)(n.p,{children:["An SDK provides a way to send RPC to a server. RPC is a call that is not related to channels at all. It's just a way to call the server method from the client-side over the real-time connection. RPC is only available when ",(0,s.jsx)(n.a,{href:"/docs/4/server/proxy#rpc-proxy",children:"RPC proxy"})," configured (so Centrifugo proxies the RPC to your application backend)."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const rpcRequest = {'key': 'value'};\nconst data = await centrifuge.namedRPC('example_method', rpcRequest);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"channel-history-api",children:"Channel history API"}),"\n",(0,s.jsx)(n.p,{children:"SDK provides a method to call publication history inside a channel (only for channels where history is enabled) to get last publications in a channel."}),"\n",(0,s.jsx)(n.p,{children:"Get stream current top position:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history();\nconsole.log(resp.offset);\nconsole.log(resp.epoch);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since known stream position:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10, since: {offset: 0, epoch: '...'}});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since current stream beginning:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.p,{children:"Get up to 10 publications from history since current stream end in reversed order (last to first):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10, reverse: true});\nconsole.log(resp.publications);\n"})}),"\n",(0,s.jsx)(n.h2,{id:"presence-and-presence-stats-api",children:"Presence and presence stats API"}),"\n",(0,s.jsxs)(n.p,{children:["Once subscribed client can call presence and presence stats information inside channel (only for channels where ",(0,s.jsx)(n.a,{href:"/docs/4/server/channels#channel-options",children:"presence configured"}),"):"]}),"\n",(0,s.jsx)(n.p,{children:"For presence (full information about active subscribers in channel):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presence();\n// resp contains presence information - a map client IDs as keys \n// and client information as values.\n"})}),"\n",(0,s.jsx)(n.p,{children:"For presence stats (just a number of clients and unique users in a channel):"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.presenceStats();\n// resp contains a number of clients and a number of unique users.\n"})}),"\n",(0,s.jsx)(n.h2,{id:"sdk-common-best-practices",children:"SDK common best practices"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Callbacks must be fast. Avoid blocking operations inside event handlers. Callbacks caused by protocol messages received from a server are called synchronously and connection read loop is blocked while such callbacks are being executed. Consider doing heavy work asynchronously."}),"\n",(0,s.jsx)(n.li,{children:"Do not blindly rely on the current Client or Subscription state when making client API calls \u2013 state can change at any moment, so don't forget to handle errors."}),"\n",(0,s.jsx)(n.li,{children:"Disconnect from a server when a mobile application goes to the background since a mobile OS can kill the connection at some point without any callbacks called."}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},85162:(e,n,i)=>{i.d(n,{Z:()=>o});i(67294);var s=i(36905);const t={tabItem:"tabItem_Ymn6"};var c=i(85893);function o(e){let{children:n,hidden:i,className:o}=e;return(0,c.jsx)("div",{role:"tabpanel",className:(0,s.Z)(t.tabItem,o),hidden:i,children:n})}},74866:(e,n,i)=>{i.d(n,{Z:()=>y});var s=i(67294),t=i(36905),c=i(12466),o=i(16550),r=i(20469),l=i(91980),a=i(67392),d=i(50012);function u(e){return s.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:i}=e;return(0,s.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:i,attributes:s,default:t}}=e;return{value:n,label:i,attributes:s,default:t}}))}(i);return function(e){const n=(0,a.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[n,i])}function p(e){let{value:n,tabValues:i}=e;return i.some((e=>e.value===n))}function b(e){let{queryString:n=!1,groupId:i}=e;const t=(0,o.k6)(),c=function(e){let{queryString:n=!1,groupId:i}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!i)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return i??null}({queryString:n,groupId:i});return[(0,l._X)(c),(0,s.useCallback)((e=>{if(!c)return;const n=new URLSearchParams(t.location.search);n.set(c,e),t.replace({...t.location,search:n.toString()})}),[c,t])]}function x(e){const{defaultValue:n,queryString:i=!1,groupId:t}=e,c=h(e),[o,l]=(0,s.useState)((()=>function(e){let{defaultValue:n,tabValues:i}=e;if(0===i.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(n){if(!p({value:n,tabValues:i}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${i.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const s=i.find((e=>e.default))??i[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:c}))),[a,u]=b({queryString:i,groupId:t}),[x,g]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,c]=(0,d.Nk)(i);return[t,(0,s.useCallback)((e=>{i&&c.set(e)}),[i,c])]}({groupId:t}),f=(()=>{const e=a??x;return p({value:e,tabValues:c})?e:null})();(0,r.Z)((()=>{f&&l(f)}),[f]);return{selectedValue:o,selectValue:(0,s.useCallback)((e=>{if(!p({value:e,tabValues:c}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),g(e)}),[u,g,c]),tabValues:c}}var g=i(72389);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=i(85893);function m(e){let{className:n,block:i,selectedValue:s,selectValue:o,tabValues:r}=e;const l=[],{blockElementScrollPositionUntilNextRender:a}=(0,c.o5)(),d=e=>{const n=e.currentTarget,i=l.indexOf(n),t=r[i].value;t!==s&&(a(n),o(t))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const i=l.indexOf(e.currentTarget)+1;n=l[i]??l[0];break}case"ArrowLeft":{const i=l.indexOf(e.currentTarget)-1;n=l[i]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":i},n),children:r.map((e=>{let{value:n,label:i,attributes:c}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:s===n?0:-1,"aria-selected":s===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...c,className:(0,t.Z)("tabs__item",f.tabItem,c?.className,{"tabs__item--active":s===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:t}=e;const c=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=c.find((e=>e.props.value===t));return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:c.map(((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function w(e){const n=x(e);return(0,j.jsxs)("div",{className:(0,t.Z)("tabs-container",f.tabList),children:[(0,j.jsx)(m,{...n,...e}),(0,j.jsx)(v,{...n,...e})]})}function y(e){const n=(0,g.Z)();return(0,j.jsx)(w,{...e,children:u(e.children)},String(n))}},52869:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/client_state-34264b7a7eee2792baa58bb5bb525d46.png"},48720:(e,n,i)=>{i.d(n,{Z:()=>s});const s=i.p+"assets/images/sub_state-9dbaf6d2a6868264a330b1a3f4c59b39.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>r,a:()=>o});var s=i(67294);const t={},c=s.createContext(t);function o(e){const n=s.useContext(c);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/369aea06.5426b854.js b/assets/js/369aea06.da51eb5e.js similarity index 96% rename from assets/js/369aea06.5426b854.js rename to assets/js/369aea06.da51eb5e.js index a3566efe6..750745684 100644 --- a/assets/js/369aea06.5426b854.js +++ b/assets/js/369aea06.da51eb5e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5859],{99305:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=n(85893),r=n(11151);const s={id:"tracing",title:"User and channel tracing"},a=void 0,o={id:"pro/tracing",title:"User and channel tracing",description:"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.",source:"@site/versioned_docs/version-4/pro/tracing.md",sourceDirName:"pro",slug:"/pro/tracing",permalink:"/docs/4/pro/tracing",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/tracing.md",tags:[],version:"4",frontMatter:{id:"tracing",title:"User and channel tracing"},sidebar:"Pro",previous:{title:"Install and run PRO version",permalink:"/docs/4/pro/install_and_run"},next:{title:"Analytics with ClickHouse",permalink:"/docs/4/pro/analytics"}},c={},l=[{value:"Save to a file",id:"save-to-a-file",level:3}];function d(e){const t={code:"code",h3:"h3",img:"img",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"tracing",src:n(27298).Z+"",width:"3862",height:"925"})}),"\n",(0,i.jsx)(t.p,{children:"It's possible to attach to trace streams using Centrifugo admin UI panel or simply from terminal using CURL and admin token."}),"\n",(0,i.jsx)(t.p,{children:"This can be super-useful for debugging issues, investigating application behavior, understanding that the application works as expected."}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/tracing_ui.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.h3,{id:"save-to-a-file",children:"Save to a file"}),"\n",(0,i.jsx)(t.p,{children:"It's possible to connect to the admin tracing endpoint with CURL using the admin session token. And then save tracing output to a file for later processing."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:'curl -X POST http://localhost:8000/admin/trace -H "Authorization: token <ADMIN_AUTH_TOKEN>" -d \'{"type": "user", "entity": "56"}\' -o trace.txt\n'})}),"\n",(0,i.jsx)(t.p,{children:"Currently, you should copy the admin auth token from browser developer tools, this may be improved in the future as PRO version evolves."})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},27298:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/tracing-fd844bdc776dc14d4061afd620b7b70b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5859],{99305:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var i=n(85893),r=n(11151);const s={id:"tracing",title:"User and channel tracing"},a=void 0,o={id:"pro/tracing",title:"User and channel tracing",description:"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.",source:"@site/versioned_docs/version-4/pro/tracing.md",sourceDirName:"pro",slug:"/pro/tracing",permalink:"/docs/4/pro/tracing",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/tracing.md",tags:[],version:"4",frontMatter:{id:"tracing",title:"User and channel tracing"},sidebar:"Pro",previous:{title:"Install and run PRO version",permalink:"/docs/4/pro/install_and_run"},next:{title:"Analytics with ClickHouse",permalink:"/docs/4/pro/analytics"}},c={},l=[{value:"Save to a file",id:"save-to-a-file",level:3}];function d(e){const t={code:"code",h3:"h3",img:"img",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"tracing",src:n(64164).Z+"",width:"3862",height:"925"})}),"\n",(0,i.jsx)(t.p,{children:"It's possible to attach to trace streams using Centrifugo admin UI panel or simply from terminal using CURL and admin token."}),"\n",(0,i.jsx)(t.p,{children:"This can be super-useful for debugging issues, investigating application behavior, understanding that the application works as expected."}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/tracing_ui.mp4",type:"video/mp4"}),(0,i.jsx)(t.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(t.h3,{id:"save-to-a-file",children:"Save to a file"}),"\n",(0,i.jsx)(t.p,{children:"It's possible to connect to the admin tracing endpoint with CURL using the admin session token. And then save tracing output to a file for later processing."}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:'curl -X POST http://localhost:8000/admin/trace -H "Authorization: token <ADMIN_AUTH_TOKEN>" -d \'{"type": "user", "entity": "56"}\' -o trace.txt\n'})}),"\n",(0,i.jsx)(t.p,{children:"Currently, you should copy the admin auth token from browser developer tools, this may be improved in the future as PRO version evolves."})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},64164:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/tracing-fd844bdc776dc14d4061afd620b7b70b.png"},11151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>a});var i=n(67294);const r={},s=i.createContext(r);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/39d4d18a.bff5a9e8.js b/assets/js/39d4d18a.40345bfc.js similarity index 99% rename from assets/js/39d4d18a.bff5a9e8.js rename to assets/js/39d4d18a.40345bfc.js index 77088591d..db8c9f0c9 100644 --- a/assets/js/39d4d18a.bff5a9e8.js +++ b/assets/js/39d4d18a.40345bfc.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5625],{52372:(e,i,n)=>{n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>c,default:()=>p,frontMatter:()=>d,metadata:()=>a,toc:()=>h});var t=n(85893),o=n(11151),r=n(67294);class s extends r.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={config:null,output:"Here will be configuration for v3",logs:"Here will be log of changes made in your config"}}onClick(e){if(!this.state.config)return void alert("Provide a configuration");let i;try{i=JSON.parse(this.state.config)}catch{return void alert("Invalid JSON")}let n=[],t=[],o=function(e){let i="config top-level";return void 0!==e&&(i="namespace {"+e.name+"}"),i},r=function(e,r,s){t.push("`"+e+"` renamed to `"+r+"`");let d=o(s);void 0===s&&(s=i),void 0===s[r]&&void 0!==s[e]&&(s[r]=s[e],delete s[e],n.push("renamed "+e+" to "+r+" in "+d))},s=function(e,r){t.push("`"+e+"` removed");let s=o(r);void 0===r&&(r=i),void 0!==r[e]&&(delete r[e],n.push("removed "+e+" from "+s))},d=function(e,r){t.push("`"+e+"` should be converted to duration");let s=o(r);if(void 0===r&&(r=i),void 0!==r[e]){let i=r[e];"number"==typeof i&&(Math.floor(i)===i?r[e]=r[e]+"s":r[e]=1e3*i+"ms",n.push("updated "+e+" to duration value "+r[e]+" in "+s))}},c=!1;for(var a in i)a.startsWith("proxy_")&&(c=!0);if(c&&void 0===i.proxy_http_headers){let e=["Origin","User-Agent","Cookie","Authorization","X-Real-Ip","X-Forwarded-For","X-Request-Id"];if(void 0!==i.proxy_extra_http_headers)for(var l in i.proxy_extra_http_headers)e.push(i.proxy_extra_http_headers[l]);i.proxy_http_headers=e,n.push("set list of headers for HTTP proxy (since v3 requires explicit values)"),s("proxy_extra_http_headers")}if(function(e,r,s){t.push("`"+e+"` is now required");let d=o(s);void 0===s&&(s=i),void 0===s[e]&&(s[e]=r,n.push("added "+e+" to "+d))}("allowed_origins",[]),s("v3_use_offset"),s("redis_streams"),s("tls_autocert_force_rsa"),s("redis_pubsub_num_workers"),s("sockjs_disable"),r("secret","token_hmac_secret_key"),r("history_lifetime","history_ttl"),r("history_recover","recover"),r("server_side","protected"),r("client_presence_ping_interval","client_presence_update_interval"),r("client_ping_interval","websocket_ping_interval"),r("client_message_write_timeout","websocket_write_timeout"),r("client_request_max_size","websocket_message_size_limit"),r("client_presence_expire_interval","presence_ttl"),r("memory_history_meta_ttl","history_meta_ttl"),r("redis_history_meta_ttl","history_meta_ttl"),r("redis_sequence_ttl","history_meta_ttl"),r("redis_presence_ttl","presence_ttl"),d("presence_ttl"),d("websocket_write_timeout"),d("websocket_ping_interval"),d("client_presence_update_interval"),d("history_ttl"),d("history_meta_ttl"),d("nats_dial_timeout"),d("nats_write_timeout"),d("graphite_interval"),d("shutdown_timeout"),d("shutdown_termination_delay"),d("proxy_connect_timeout"),d("proxy_refresh_timeout"),d("proxy_rpc_timeout"),d("proxy_subscribe_timeout"),d("proxy_publish_timeout"),d("client_expired_close_delay"),d("client_expired_sub_close_delay"),d("client_stale_close_delay"),d("client_channel_position_check_delay"),d("node_info_metrics_aggregate_interval"),d("websocket_ping_interval"),d("websocket_write_timeout"),d("sockjs_heartbeat_delay"),d("redis_idle_timeout"),d("redis_connect_timeout"),d("redis_read_timeout"),d("redis_write_timeout"),void 0!==i.namespaces){let e=[];for(let n of i.namespaces)r("history_lifetime","history_ttl",n),d("history_ttl",n),r("history_recover","recover",n),r("server_side","protected",n),e.push(n);i.namespaces=e}if(void 0!==i.redis_host&&void 0!==i.redis_port){let e=[],t=i.redis_host.toString().split(","),o=i.redis_port.toString().split(",");if(t.length!==o.length)return void alert("Sorry, too difficult Redis configuration to automatically convert");for(let i in t){let n=t[i]+":"+o[i];e.push(n)}i.redis_address=e,s("redis_host"),s("redis_port"),n.push("redis configuration updated, but you should check it manually")}else void 0!==i.redis_url&&r("redis_url","redis_address");r("redis_cluster_addrs","redis_cluster_address"),r("redis_sentinels","redis_sentinel_address"),r("redis_master_name","redis_sentinel_master_name"),this.setState({output:JSON.stringify(i,null,"\t")}),this.setState({logs:JSON.stringify(n,null,"\t")}),console.log(t.join("\n\n"))}onChange(e){this.setState({config:e.target.value})}render(){return(0,t.jsxs)("div",{children:[(0,t.jsx)("textarea",{onChange:this.onChange,placeholder:"Paste your Centrifugo v2 JSON config here...",style:{width:"100%",height:"300px",border:"2px solid #ccc"}}),(0,t.jsx)("button",{onClick:this.onClick,children:"Convert"}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.output}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.logs})]})}}const d={id:"migration_v3",title:"Migrating to v3"},c=void 0,a={id:"getting-started/migration_v3",title:"Migrating to v3",description:"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.",source:"@site/versioned_docs/version-3/getting-started/migration-v3.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v3",permalink:"/docs/3/getting-started/migration_v3",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/migration-v3.md",tags:[],version:"3",frontMatter:{id:"migration_v3",title:"Migrating to v3"},sidebar:"Introduction",previous:{title:"Design overview",permalink:"/docs/3/getting-started/design"}},l={},h=[{value:"Client-side changes",id:"client-side-changes",level:2},{value:"No unlimited history by default",id:"no-unlimited-history-by-default",level:3},{value:"Publication limit for recovery",id:"publication-limit-for-recovery",level:3},{value:"Seq/Gen fields removed",id:"seqgen-fields-removed",level:3},{value:"Server-side changes",id:"server-side-changes",level:2},{value:"Time interval options are duration",id:"time-interval-options-are-duration",level:3},{value:"Channel options changes",id:"channel-options-changes",level:3},{value:"Some command-line flags removed",id:"some-command-line-flags-removed",level:3},{value:"Enforced request Origin check",id:"enforced-request-origin-check",level:3},{value:"Updated GRPC API Protobuf package",id:"updated-grpc-api-protobuf-package",level:3},{value:"Channels API method changed",id:"channels-api-method-changed",level:3},{value:"HTTP proxy changes",id:"http-proxy-changes",level:3},{value:"JWT changes",id:"jwt-changes",level:3},{value:"Redis configuration changes",id:"redis-configuration-changes",level:3},{value:"Redis streams used by default",id:"redis-streams-used-by-default",level:3},{value:"SockJS disabled by default",id:"sockjs-disabled-by-default",level:3},{value:"Other configuration changes",id:"other-configuration-changes",level:3},{value:"v2 to v3 config converter",id:"v2-to-v3-config-converter",level:3}];function u(e){const i={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.p,{children:"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."}),"\n",(0,t.jsx)(i.p,{children:"There are a couple of exceptions - read carefully about client-side changes below."}),"\n",(0,t.jsx)(i.p,{children:"In any case \u2013 don't forget to test your application before running in production."}),"\n",(0,t.jsx)(i.h2,{id:"client-side-changes",children:"Client-side changes"}),"\n",(0,t.jsx)(i.p,{children:"Client protocol has some backward incompatible changes regarding working with history API and removing deprecated fields."}),"\n",(0,t.jsx)(i.h3,{id:"no-unlimited-history-by-default",children:"No unlimited history by default"}),"\n",(0,t.jsxs)(i.p,{children:["Call to ",(0,t.jsx)(i.code,{children:"history"})," API from client-side now does not return all publications from history cache. It returns only information about a stream with zero publications. Clients should explicitly provide a limit when calling history API. Also, the maximum allowed limit can be set by ",(0,t.jsx)(i.code,{children:"client_history_max_publication_limit"})," option (by default ",(0,t.jsx)(i.code,{children:"300"}),")."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a boolean flag ",(0,t.jsx)(i.code,{children:"use_unlimited_history_by_default"})," on configuration file top level to enable previous behavior while you migrate client applications to use explicit limit."]}),"\n",(0,t.jsx)(i.h3,{id:"publication-limit-for-recovery",children:"Publication limit for recovery"}),"\n",(0,t.jsxs)(i.p,{children:["The maximum number of messages that can be recovered is now limited by ",(0,t.jsx)(i.code,{children:"client_recovery_max_publication_limit"})," option which is by default ",(0,t.jsx)(i.code,{children:"300"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"seqgen-fields-removed",children:"Seq/Gen fields removed"}),"\n",(0,t.jsxs)(i.p,{children:["Deprecated seq/gen now removed and Centrifugo uses ",(0,t.jsx)(i.code,{children:"offset"})," field for a position in a stream. This means that there is no need for ",(0,t.jsx)(i.code,{children:"v3_use_offset"})," option anymore \u2013 it's not used in Centrifugo v3."]}),"\n",(0,t.jsx)(i.h2,{id:"server-side-changes",children:"Server-side changes"}),"\n",(0,t.jsx)(i.h3,{id:"time-interval-options-are-duration",children:"Time interval options are duration"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 all time intervals should be configured using ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["For example ",(0,t.jsx)(i.code,{children:'"proxy_connect_timeout": 1'})," should be changed to ",(0,t.jsx)(i.code,{children:'"proxy_connect_timeout": "1s"'}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"})," which takes this change into account."]}),"\n",(0,t.jsx)(i.h3,{id:"channel-options-changes",children:"Channel options changes"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 ",(0,t.jsx)(i.code,{children:"history_recover"})," option becomes ",(0,t.jsx)(i.code,{children:"recover"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["Option ",(0,t.jsx)(i.code,{children:"history_lifetime"})," renamed to ",(0,t.jsx)(i.code,{children:"history_ttl"})," and it's now a ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["Option ",(0,t.jsx)(i.code,{children:"server_side"})," removed, see ",(0,t.jsx)(i.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option as a replacement."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"})," which takes these changes into account."]}),"\n",(0,t.jsx)(i.h3,{id:"some-command-line-flags-removed",children:"Some command-line flags removed"}),"\n",(0,t.jsx)(i.p,{children:"Configuring over command-line flags is not very convenient for production deployments, Centrifugo v3 reduced the number of command-line flags available \u2013 it mostly has flags frequently useful for development now."}),"\n",(0,t.jsx)(i.h3,{id:"enforced-request-origin-check",children:"Enforced request Origin check"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 you should explicitly ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#allowed_origins",children:"set a list of allowed origins"})," which are allowed to connect to client transport endpoints."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "allowed_origins": ["https://mysite.com"]\n}\n'})}),"\n",(0,t.jsxs)(i.p,{children:["There is a way to disable origin check, but it's discouraged and ",(0,t.jsx)(i.strong,{children:"insecure"})," in case you are using connect proxy feature."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "allowed_origins": ["*"]\n}\n'})}),"\n",(0,t.jsx)(i.h3,{id:"updated-grpc-api-protobuf-package",children:"Updated GRPC API Protobuf package"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 we addressed an ",(0,t.jsx)(i.a,{href:"https://github.com/centrifugal/centrifugo/issues/379",children:"issue"})," where package name in Protobuf definitions resulted in some inconvenience and attempts to rename it. But it's not possible to rename it since GRPC uses it as part of RPC methods internally. Now GRPC API package looks like this:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{children:"package centrifugal.centrifugo.api;\n"})}),"\n",(0,t.jsxs)(i.p,{children:["This means you need to regenerate your GRPC code which communicates with Centrifugo using the latest Protobuf definitions. Refer to the ",(0,t.jsx)(i.a,{href:"/docs/3/server/server_api#grpc-api",children:"GRPC API doc"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"channels-api-method-changed",children:"Channels API method changed"}),"\n",(0,t.jsxs)(i.p,{children:["The response format of ",(0,t.jsx)(i.code,{children:"channels"})," API call changed in v3. See description in ",(0,t.jsx)(i.a,{href:"/docs/3/server/server_api#channels",children:"API doc"}),"."]}),"\n",(0,t.jsx)(i.p,{children:"The channels method has new additional possibilities like showing the number of connections in a channel and filter channels by pattern."}),"\n",(0,t.jsx)(i.admonition,{type:"info",children:(0,t.jsx)(i.p,{children:"Channels API call still has the same concern as before: this method does not scale well for many active channels in a system and is mostly recommended for administrative/debug purposes."})}),"\n",(0,t.jsx)(i.h3,{id:"http-proxy-changes",children:"HTTP proxy changes"}),"\n",(0,t.jsx)(i.p,{children:"When using HTTP proxy you should now set an explicit list of headers you want to proxy. To mimic the behavior of Centrifugo v2 add to your configuration:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:"title=config.json",children:'{\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,t.jsxs)(i.p,{children:["If you had a list of extra HTTP headers using ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"})," then additionally extend list above with values from ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"}),". Then you can remove ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"})," - it's not used anymore."]}),"\n",(0,t.jsxs)(i.p,{children:["Another important change is how Centrifugo proxies binary data over HTTP JSON proxy. Previously proxy mode (whether to use base64 fields or not) could be configured using ",(0,t.jsx)(i.code,{children:"encoding=binary"})," URL param of connection. With Centrifugo v3 it's only possible to use binary mode by enabling ",(0,t.jsx)(i.code,{children:'"proxy_binary_encoding": true'})," option. BTW according to our community poll only 2% of Centrifugo users used binary mode in HTTP proxy. If you have problems with new behavior \u2013 write about your situation to our community chats \u2013 and we will see what's possible."]}),"\n",(0,t.jsx)(i.h3,{id:"jwt-changes",children:"JWT changes"}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"eto"})," claim of subscription JWT removed. But since Centrifugo v3 introduced an additional ",(0,t.jsx)(i.code,{children:"expire_at"})," claim it's still possible to implement one-time subscription tokens without enabling subscription expiration workflow by setting ",(0,t.jsx)(i.code,{children:'"expire_at: 0"'})," in subscription JWT claims."]}),"\n",(0,t.jsx)(i.h3,{id:"redis-configuration-changes",children:"Redis configuration changes"}),"\n",(0,t.jsx)(i.p,{children:"Redis configuration was a bit messy - especially in the Redis sharding case, in v3 we decided to clean up it a bit. Make it more explicit and reduce the number of possible ways to configure."}),"\n",(0,t.jsxs)(i.p,{children:["Refer to the ",(0,t.jsx)(i.a,{href:"/docs/3/server/engines#redis-engine",children:"Redis Engine docs"})," for the new configuration details. The important thing is that there is no separate ",(0,t.jsx)(i.code,{children:"redis_host"})," and ",(0,t.jsx)(i.code,{children:"redis_port"})," option anymore \u2013 those are replaced with single ",(0,t.jsx)(i.code,{children:"redis_address"})," option."]}),"\n",(0,t.jsx)(i.h3,{id:"redis-streams-used-by-default",children:"Redis streams used by default"}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo v3 will use Redis Stream data structure to keep history instead of lists."}),"\n",(0,t.jsx)(i.admonition,{type:"danger",children:(0,t.jsxs)(i.p,{children:["This requires Redis >= 5.0.1 to work. If you still need List data structure or have an old Redis version you can use ",(0,t.jsx)(i.code,{children:'"redis_use_lists": true'})," to mimic the default behavior of Centrifugo v2."]})}),"\n",(0,t.jsx)(i.h3,{id:"sockjs-disabled-by-default",children:"SockJS disabled by default"}),"\n",(0,t.jsxs)(i.p,{children:["Our poll showed that most Centrifugo users do not use SockJS transport. In v3 it's disabled by default. You can enable it by setting ",(0,t.jsx)(i.code,{children:'"sockjs": true'})," in configuration."]}),"\n",(0,t.jsx)(i.h3,{id:"other-configuration-changes",children:"Other configuration changes"}),"\n",(0,t.jsxs)(i.p,{children:["Here is a full list of configuration option changes. We provide a best-effort ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"allowed_origins"})," is now required to be set to authorize requests with ",(0,t.jsx)(i.code,{children:"Origin"})," header"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"v3_use_offset"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_streams"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"tls_autocert_force_rsa"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_pubsub_num_workers"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"sockjs_disable"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"secret"})," renamed to ",(0,t.jsx)(i.code,{children:"token_hmac_secret_key"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_lifetime"})," renamed to ",(0,t.jsx)(i.code,{children:"history_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_recover"})," renamed to ",(0,t.jsx)(i.code,{children:"recover"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_ping_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"client_presence_update_interval"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_ping_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_ping_interval"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_message_write_timeout"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_write_timeout"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_request_max_size"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_message_size_limit"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_expire_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"presence_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"memory_history_meta_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_history_meta_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_sequence_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_presence_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"presence_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"presence_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_ping_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_update_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_meta_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"nats_dial_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"nats_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"graphite_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"shutdown_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"shutdown_termination_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_connect_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_refresh_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_rpc_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_subscribe_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_publish_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_expired_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_expired_sub_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_stale_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_channel_position_check_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"node_info_metrics_aggregate_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_ping_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"sockjs_heartbeat_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_idle_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_connect_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_read_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_cluster_addrs"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_cluster_address"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_sentinels"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_sentinel_address"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_master_name"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_sentinel_master_name"})]}),"\n",(0,t.jsx)(i.h3,{id:"v2-to-v3-config-converter",children:"v2 to v3 config converter"}),"\n",(0,t.jsx)(i.p,{children:"Here is a converter between Centrifugo v2 and v3 JSON configuration. It can help to translate most of the things automatically for you."}),"\n",(0,t.jsxs)(i.p,{children:["If you are using Centrifugo with TOML format then you can use ",(0,t.jsx)(i.a,{href:"https://pseitz.github.io/toml-to-json-online-converter/",children:"online converter"})," as initial step. Or ",(0,t.jsx)(i.a,{href:"https://jsonformatter.org/yaml-to-json",children:"yaml-to-json"})," and ",(0,t.jsx)(i.a,{href:"https://jsonformatter.org/json-to-yaml",children:"json-to-yaml"})," for YAML."]}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"It's fully client-side: your data won't be sent anywhere."})}),"\n",(0,t.jsx)(i.admonition,{type:"danger",children:(0,t.jsxs)(i.p,{children:["Unfortunately, we can't migrate environment variables and command-line flags automatically - so if you are using env vars or command-line flags to configure Centrifugo you still need to migrate manually. Also, be aware: this converter tool is the best effort only \u2013 we can not guarantee it solves all corner cases, especially in Redis configuration. You may still need to fix some things manually, for example - properly fill ",(0,t.jsx)(i.code,{children:"allowed_origins"}),"."]})}),"\n","\n",(0,t.jsx)(s,{})]})}function p(e={}){const{wrapper:i}={...(0,o.a)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},11151:(e,i,n)=>{n.d(i,{Z:()=>d,a:()=>s});var t=n(67294);const o={},r=t.createContext(o);function s(e){const i=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(i):{...i,...e}}),[i,e])}function d(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),t.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5625],{94989:(e,i,n)=>{n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>c,default:()=>p,frontMatter:()=>d,metadata:()=>a,toc:()=>h});var t=n(85893),o=n(11151),r=n(67294);class s extends r.Component{constructor(){super(),this.onChange=this.onChange.bind(this),this.onClick=this.onClick.bind(this),this.state={config:null,output:"Here will be configuration for v3",logs:"Here will be log of changes made in your config"}}onClick(e){if(!this.state.config)return void alert("Provide a configuration");let i;try{i=JSON.parse(this.state.config)}catch{return void alert("Invalid JSON")}let n=[],t=[],o=function(e){let i="config top-level";return void 0!==e&&(i="namespace {"+e.name+"}"),i},r=function(e,r,s){t.push("`"+e+"` renamed to `"+r+"`");let d=o(s);void 0===s&&(s=i),void 0===s[r]&&void 0!==s[e]&&(s[r]=s[e],delete s[e],n.push("renamed "+e+" to "+r+" in "+d))},s=function(e,r){t.push("`"+e+"` removed");let s=o(r);void 0===r&&(r=i),void 0!==r[e]&&(delete r[e],n.push("removed "+e+" from "+s))},d=function(e,r){t.push("`"+e+"` should be converted to duration");let s=o(r);if(void 0===r&&(r=i),void 0!==r[e]){let i=r[e];"number"==typeof i&&(Math.floor(i)===i?r[e]=r[e]+"s":r[e]=1e3*i+"ms",n.push("updated "+e+" to duration value "+r[e]+" in "+s))}},c=!1;for(var a in i)a.startsWith("proxy_")&&(c=!0);if(c&&void 0===i.proxy_http_headers){let e=["Origin","User-Agent","Cookie","Authorization","X-Real-Ip","X-Forwarded-For","X-Request-Id"];if(void 0!==i.proxy_extra_http_headers)for(var l in i.proxy_extra_http_headers)e.push(i.proxy_extra_http_headers[l]);i.proxy_http_headers=e,n.push("set list of headers for HTTP proxy (since v3 requires explicit values)"),s("proxy_extra_http_headers")}if(function(e,r,s){t.push("`"+e+"` is now required");let d=o(s);void 0===s&&(s=i),void 0===s[e]&&(s[e]=r,n.push("added "+e+" to "+d))}("allowed_origins",[]),s("v3_use_offset"),s("redis_streams"),s("tls_autocert_force_rsa"),s("redis_pubsub_num_workers"),s("sockjs_disable"),r("secret","token_hmac_secret_key"),r("history_lifetime","history_ttl"),r("history_recover","recover"),r("server_side","protected"),r("client_presence_ping_interval","client_presence_update_interval"),r("client_ping_interval","websocket_ping_interval"),r("client_message_write_timeout","websocket_write_timeout"),r("client_request_max_size","websocket_message_size_limit"),r("client_presence_expire_interval","presence_ttl"),r("memory_history_meta_ttl","history_meta_ttl"),r("redis_history_meta_ttl","history_meta_ttl"),r("redis_sequence_ttl","history_meta_ttl"),r("redis_presence_ttl","presence_ttl"),d("presence_ttl"),d("websocket_write_timeout"),d("websocket_ping_interval"),d("client_presence_update_interval"),d("history_ttl"),d("history_meta_ttl"),d("nats_dial_timeout"),d("nats_write_timeout"),d("graphite_interval"),d("shutdown_timeout"),d("shutdown_termination_delay"),d("proxy_connect_timeout"),d("proxy_refresh_timeout"),d("proxy_rpc_timeout"),d("proxy_subscribe_timeout"),d("proxy_publish_timeout"),d("client_expired_close_delay"),d("client_expired_sub_close_delay"),d("client_stale_close_delay"),d("client_channel_position_check_delay"),d("node_info_metrics_aggregate_interval"),d("websocket_ping_interval"),d("websocket_write_timeout"),d("sockjs_heartbeat_delay"),d("redis_idle_timeout"),d("redis_connect_timeout"),d("redis_read_timeout"),d("redis_write_timeout"),void 0!==i.namespaces){let e=[];for(let n of i.namespaces)r("history_lifetime","history_ttl",n),d("history_ttl",n),r("history_recover","recover",n),r("server_side","protected",n),e.push(n);i.namespaces=e}if(void 0!==i.redis_host&&void 0!==i.redis_port){let e=[],t=i.redis_host.toString().split(","),o=i.redis_port.toString().split(",");if(t.length!==o.length)return void alert("Sorry, too difficult Redis configuration to automatically convert");for(let i in t){let n=t[i]+":"+o[i];e.push(n)}i.redis_address=e,s("redis_host"),s("redis_port"),n.push("redis configuration updated, but you should check it manually")}else void 0!==i.redis_url&&r("redis_url","redis_address");r("redis_cluster_addrs","redis_cluster_address"),r("redis_sentinels","redis_sentinel_address"),r("redis_master_name","redis_sentinel_master_name"),this.setState({output:JSON.stringify(i,null,"\t")}),this.setState({logs:JSON.stringify(n,null,"\t")}),console.log(t.join("\n\n"))}onChange(e){this.setState({config:e.target.value})}render(){return(0,t.jsxs)("div",{children:[(0,t.jsx)("textarea",{onChange:this.onChange,placeholder:"Paste your Centrifugo v2 JSON config here...",style:{width:"100%",height:"300px",border:"2px solid #ccc"}}),(0,t.jsx)("button",{onClick:this.onClick,children:"Convert"}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.output}),(0,t.jsx)("pre",{style:{marginTop:"10px"},children:this.state.logs})]})}}const d={id:"migration_v3",title:"Migrating to v3"},c=void 0,a={id:"getting-started/migration_v3",title:"Migrating to v3",description:"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.",source:"@site/versioned_docs/version-3/getting-started/migration-v3.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v3",permalink:"/docs/3/getting-started/migration_v3",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/migration-v3.md",tags:[],version:"3",frontMatter:{id:"migration_v3",title:"Migrating to v3"},sidebar:"Introduction",previous:{title:"Design overview",permalink:"/docs/3/getting-started/design"}},l={},h=[{value:"Client-side changes",id:"client-side-changes",level:2},{value:"No unlimited history by default",id:"no-unlimited-history-by-default",level:3},{value:"Publication limit for recovery",id:"publication-limit-for-recovery",level:3},{value:"Seq/Gen fields removed",id:"seqgen-fields-removed",level:3},{value:"Server-side changes",id:"server-side-changes",level:2},{value:"Time interval options are duration",id:"time-interval-options-are-duration",level:3},{value:"Channel options changes",id:"channel-options-changes",level:3},{value:"Some command-line flags removed",id:"some-command-line-flags-removed",level:3},{value:"Enforced request Origin check",id:"enforced-request-origin-check",level:3},{value:"Updated GRPC API Protobuf package",id:"updated-grpc-api-protobuf-package",level:3},{value:"Channels API method changed",id:"channels-api-method-changed",level:3},{value:"HTTP proxy changes",id:"http-proxy-changes",level:3},{value:"JWT changes",id:"jwt-changes",level:3},{value:"Redis configuration changes",id:"redis-configuration-changes",level:3},{value:"Redis streams used by default",id:"redis-streams-used-by-default",level:3},{value:"SockJS disabled by default",id:"sockjs-disabled-by-default",level:3},{value:"Other configuration changes",id:"other-configuration-changes",level:3},{value:"v2 to v3 config converter",id:"v2-to-v3-config-converter",level:3}];function u(e){const i={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.p,{children:"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."}),"\n",(0,t.jsx)(i.p,{children:"There are a couple of exceptions - read carefully about client-side changes below."}),"\n",(0,t.jsx)(i.p,{children:"In any case \u2013 don't forget to test your application before running in production."}),"\n",(0,t.jsx)(i.h2,{id:"client-side-changes",children:"Client-side changes"}),"\n",(0,t.jsx)(i.p,{children:"Client protocol has some backward incompatible changes regarding working with history API and removing deprecated fields."}),"\n",(0,t.jsx)(i.h3,{id:"no-unlimited-history-by-default",children:"No unlimited history by default"}),"\n",(0,t.jsxs)(i.p,{children:["Call to ",(0,t.jsx)(i.code,{children:"history"})," API from client-side now does not return all publications from history cache. It returns only information about a stream with zero publications. Clients should explicitly provide a limit when calling history API. Also, the maximum allowed limit can be set by ",(0,t.jsx)(i.code,{children:"client_history_max_publication_limit"})," option (by default ",(0,t.jsx)(i.code,{children:"300"}),")."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a boolean flag ",(0,t.jsx)(i.code,{children:"use_unlimited_history_by_default"})," on configuration file top level to enable previous behavior while you migrate client applications to use explicit limit."]}),"\n",(0,t.jsx)(i.h3,{id:"publication-limit-for-recovery",children:"Publication limit for recovery"}),"\n",(0,t.jsxs)(i.p,{children:["The maximum number of messages that can be recovered is now limited by ",(0,t.jsx)(i.code,{children:"client_recovery_max_publication_limit"})," option which is by default ",(0,t.jsx)(i.code,{children:"300"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"seqgen-fields-removed",children:"Seq/Gen fields removed"}),"\n",(0,t.jsxs)(i.p,{children:["Deprecated seq/gen now removed and Centrifugo uses ",(0,t.jsx)(i.code,{children:"offset"})," field for a position in a stream. This means that there is no need for ",(0,t.jsx)(i.code,{children:"v3_use_offset"})," option anymore \u2013 it's not used in Centrifugo v3."]}),"\n",(0,t.jsx)(i.h2,{id:"server-side-changes",children:"Server-side changes"}),"\n",(0,t.jsx)(i.h3,{id:"time-interval-options-are-duration",children:"Time interval options are duration"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 all time intervals should be configured using ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["For example ",(0,t.jsx)(i.code,{children:'"proxy_connect_timeout": 1'})," should be changed to ",(0,t.jsx)(i.code,{children:'"proxy_connect_timeout": "1s"'}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"})," which takes this change into account."]}),"\n",(0,t.jsx)(i.h3,{id:"channel-options-changes",children:"Channel options changes"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 ",(0,t.jsx)(i.code,{children:"history_recover"})," option becomes ",(0,t.jsx)(i.code,{children:"recover"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["Option ",(0,t.jsx)(i.code,{children:"history_lifetime"})," renamed to ",(0,t.jsx)(i.code,{children:"history_ttl"})," and it's now a ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:["Option ",(0,t.jsx)(i.code,{children:"server_side"})," removed, see ",(0,t.jsx)(i.a,{href:"/docs/3/server/channels#protected",children:"protected"})," option as a replacement."]}),"\n",(0,t.jsxs)(i.p,{children:["We provide a ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"})," which takes these changes into account."]}),"\n",(0,t.jsx)(i.h3,{id:"some-command-line-flags-removed",children:"Some command-line flags removed"}),"\n",(0,t.jsx)(i.p,{children:"Configuring over command-line flags is not very convenient for production deployments, Centrifugo v3 reduced the number of command-line flags available \u2013 it mostly has flags frequently useful for development now."}),"\n",(0,t.jsx)(i.h3,{id:"enforced-request-origin-check",children:"Enforced request Origin check"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 you should explicitly ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#allowed_origins",children:"set a list of allowed origins"})," which are allowed to connect to client transport endpoints."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "allowed_origins": ["https://mysite.com"]\n}\n'})}),"\n",(0,t.jsxs)(i.p,{children:["There is a way to disable origin check, but it's discouraged and ",(0,t.jsx)(i.strong,{children:"insecure"})," in case you are using connect proxy feature."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "allowed_origins": ["*"]\n}\n'})}),"\n",(0,t.jsx)(i.h3,{id:"updated-grpc-api-protobuf-package",children:"Updated GRPC API Protobuf package"}),"\n",(0,t.jsxs)(i.p,{children:["In Centrifugo v3 we addressed an ",(0,t.jsx)(i.a,{href:"https://github.com/centrifugal/centrifugo/issues/379",children:"issue"})," where package name in Protobuf definitions resulted in some inconvenience and attempts to rename it. But it's not possible to rename it since GRPC uses it as part of RPC methods internally. Now GRPC API package looks like this:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{children:"package centrifugal.centrifugo.api;\n"})}),"\n",(0,t.jsxs)(i.p,{children:["This means you need to regenerate your GRPC code which communicates with Centrifugo using the latest Protobuf definitions. Refer to the ",(0,t.jsx)(i.a,{href:"/docs/3/server/server_api#grpc-api",children:"GRPC API doc"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"channels-api-method-changed",children:"Channels API method changed"}),"\n",(0,t.jsxs)(i.p,{children:["The response format of ",(0,t.jsx)(i.code,{children:"channels"})," API call changed in v3. See description in ",(0,t.jsx)(i.a,{href:"/docs/3/server/server_api#channels",children:"API doc"}),"."]}),"\n",(0,t.jsx)(i.p,{children:"The channels method has new additional possibilities like showing the number of connections in a channel and filter channels by pattern."}),"\n",(0,t.jsx)(i.admonition,{type:"info",children:(0,t.jsx)(i.p,{children:"Channels API call still has the same concern as before: this method does not scale well for many active channels in a system and is mostly recommended for administrative/debug purposes."})}),"\n",(0,t.jsx)(i.h3,{id:"http-proxy-changes",children:"HTTP proxy changes"}),"\n",(0,t.jsx)(i.p,{children:"When using HTTP proxy you should now set an explicit list of headers you want to proxy. To mimic the behavior of Centrifugo v2 add to your configuration:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:"title=config.json",children:'{\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,t.jsxs)(i.p,{children:["If you had a list of extra HTTP headers using ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"})," then additionally extend list above with values from ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"}),". Then you can remove ",(0,t.jsx)(i.code,{children:"proxy_extra_http_headers"})," - it's not used anymore."]}),"\n",(0,t.jsxs)(i.p,{children:["Another important change is how Centrifugo proxies binary data over HTTP JSON proxy. Previously proxy mode (whether to use base64 fields or not) could be configured using ",(0,t.jsx)(i.code,{children:"encoding=binary"})," URL param of connection. With Centrifugo v3 it's only possible to use binary mode by enabling ",(0,t.jsx)(i.code,{children:'"proxy_binary_encoding": true'})," option. BTW according to our community poll only 2% of Centrifugo users used binary mode in HTTP proxy. If you have problems with new behavior \u2013 write about your situation to our community chats \u2013 and we will see what's possible."]}),"\n",(0,t.jsx)(i.h3,{id:"jwt-changes",children:"JWT changes"}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"eto"})," claim of subscription JWT removed. But since Centrifugo v3 introduced an additional ",(0,t.jsx)(i.code,{children:"expire_at"})," claim it's still possible to implement one-time subscription tokens without enabling subscription expiration workflow by setting ",(0,t.jsx)(i.code,{children:'"expire_at: 0"'})," in subscription JWT claims."]}),"\n",(0,t.jsx)(i.h3,{id:"redis-configuration-changes",children:"Redis configuration changes"}),"\n",(0,t.jsx)(i.p,{children:"Redis configuration was a bit messy - especially in the Redis sharding case, in v3 we decided to clean up it a bit. Make it more explicit and reduce the number of possible ways to configure."}),"\n",(0,t.jsxs)(i.p,{children:["Refer to the ",(0,t.jsx)(i.a,{href:"/docs/3/server/engines#redis-engine",children:"Redis Engine docs"})," for the new configuration details. The important thing is that there is no separate ",(0,t.jsx)(i.code,{children:"redis_host"})," and ",(0,t.jsx)(i.code,{children:"redis_port"})," option anymore \u2013 those are replaced with single ",(0,t.jsx)(i.code,{children:"redis_address"})," option."]}),"\n",(0,t.jsx)(i.h3,{id:"redis-streams-used-by-default",children:"Redis streams used by default"}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo v3 will use Redis Stream data structure to keep history instead of lists."}),"\n",(0,t.jsx)(i.admonition,{type:"danger",children:(0,t.jsxs)(i.p,{children:["This requires Redis >= 5.0.1 to work. If you still need List data structure or have an old Redis version you can use ",(0,t.jsx)(i.code,{children:'"redis_use_lists": true'})," to mimic the default behavior of Centrifugo v2."]})}),"\n",(0,t.jsx)(i.h3,{id:"sockjs-disabled-by-default",children:"SockJS disabled by default"}),"\n",(0,t.jsxs)(i.p,{children:["Our poll showed that most Centrifugo users do not use SockJS transport. In v3 it's disabled by default. You can enable it by setting ",(0,t.jsx)(i.code,{children:'"sockjs": true'})," in configuration."]}),"\n",(0,t.jsx)(i.h3,{id:"other-configuration-changes",children:"Other configuration changes"}),"\n",(0,t.jsxs)(i.p,{children:["Here is a full list of configuration option changes. We provide a best-effort ",(0,t.jsx)(i.a,{href:"#v2-to-v3-config-converter",children:"configuration converter"}),"."]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"allowed_origins"})," is now required to be set to authorize requests with ",(0,t.jsx)(i.code,{children:"Origin"})," header"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"v3_use_offset"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_streams"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"tls_autocert_force_rsa"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_pubsub_num_workers"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"sockjs_disable"})," removed"]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"secret"})," renamed to ",(0,t.jsx)(i.code,{children:"token_hmac_secret_key"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_lifetime"})," renamed to ",(0,t.jsx)(i.code,{children:"history_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_recover"})," renamed to ",(0,t.jsx)(i.code,{children:"recover"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_ping_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"client_presence_update_interval"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_ping_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_ping_interval"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_message_write_timeout"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_write_timeout"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_request_max_size"})," renamed to ",(0,t.jsx)(i.code,{children:"websocket_message_size_limit"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_expire_interval"})," renamed to ",(0,t.jsx)(i.code,{children:"presence_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"memory_history_meta_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_history_meta_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_sequence_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"history_meta_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_presence_ttl"})," renamed to ",(0,t.jsx)(i.code,{children:"presence_ttl"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"presence_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_ping_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_presence_update_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"history_meta_ttl"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"nats_dial_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"nats_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"graphite_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"shutdown_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"shutdown_termination_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_connect_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_refresh_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_rpc_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_subscribe_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"proxy_publish_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_expired_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_expired_sub_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_stale_close_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"client_channel_position_check_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"node_info_metrics_aggregate_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_ping_interval"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"websocket_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"sockjs_heartbeat_delay"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_idle_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_connect_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_read_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_write_timeout"})," should be converted to ",(0,t.jsx)(i.a,{href:"/docs/3/server/configuration#setting-time-duration-options",children:"duration"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_cluster_addrs"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_cluster_address"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_sentinels"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_sentinel_address"})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"redis_master_name"})," renamed to ",(0,t.jsx)(i.code,{children:"redis_sentinel_master_name"})]}),"\n",(0,t.jsx)(i.h3,{id:"v2-to-v3-config-converter",children:"v2 to v3 config converter"}),"\n",(0,t.jsx)(i.p,{children:"Here is a converter between Centrifugo v2 and v3 JSON configuration. It can help to translate most of the things automatically for you."}),"\n",(0,t.jsxs)(i.p,{children:["If you are using Centrifugo with TOML format then you can use ",(0,t.jsx)(i.a,{href:"https://pseitz.github.io/toml-to-json-online-converter/",children:"online converter"})," as initial step. Or ",(0,t.jsx)(i.a,{href:"https://jsonformatter.org/yaml-to-json",children:"yaml-to-json"})," and ",(0,t.jsx)(i.a,{href:"https://jsonformatter.org/json-to-yaml",children:"json-to-yaml"})," for YAML."]}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"It's fully client-side: your data won't be sent anywhere."})}),"\n",(0,t.jsx)(i.admonition,{type:"danger",children:(0,t.jsxs)(i.p,{children:["Unfortunately, we can't migrate environment variables and command-line flags automatically - so if you are using env vars or command-line flags to configure Centrifugo you still need to migrate manually. Also, be aware: this converter tool is the best effort only \u2013 we can not guarantee it solves all corner cases, especially in Redis configuration. You may still need to fix some things manually, for example - properly fill ",(0,t.jsx)(i.code,{children:"allowed_origins"}),"."]})}),"\n","\n",(0,t.jsx)(s,{})]})}function p(e={}){const{wrapper:i}={...(0,o.a)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},11151:(e,i,n)=>{n.d(i,{Z:()=>d,a:()=>s});var t=n(67294);const o={},r=t.createContext(o);function s(e){const i=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(i):{...i,...e}}),[i,e])}function d(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),t.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3a2ce571.fc9e7f55.js b/assets/js/3a2ce571.c475ab47.js similarity index 84% rename from assets/js/3a2ce571.fc9e7f55.js rename to assets/js/3a2ce571.c475ab47.js index 006ed84c1..93828d143 100644 --- a/assets/js/3a2ce571.fc9e7f55.js +++ b/assets/js/3a2ce571.c475ab47.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5077],{87210:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>d});var s=t(85893),i=t(11151);const o={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:!1},r=void 0,a={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:!1,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:!1},unlisted:!1,prevItem:{title:"Proper real-time document state synchronization within Centrifugal ecosystem",permalink:"/blog/2024/06/03/real-time-document-state-sync"},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"}},l={authorsImageUrls:[void 0]},d=[{value:"About delta compression",id:"about-delta-compression",level:2},{value:"Experiment Overview",id:"experiment-overview",level:2},{value:"Results Breakdown",id:"results-breakdown",level:2},{value:"Results analysis",id:"results-analysis",level:2},{value:"JSON over JSON",id:"json-over-json",level:3},{value:"JSON over Protobuf",id:"json-over-protobuf",level:3},{value:"Protobuf over Protobuf",id:"protobuf-over-protobuf",level:3},{value:"Conclusion",id:"conclusion",level:2}];function c(e){const n={a:"a",code:"code",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components},{Details:o}=n;return o||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("img",{src:"/img/football_match_compression.jpg"}),"\n",(0,s.jsx)(n.p,{children:"Optimizing 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",(0,s.jsxs)(n.p,{children:["Centrifugo v5.4.0 introduced ",(0,s.jsx)(n.a,{href:"/docs/server/delta_compression",children:"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",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.h2,{id:"about-delta-compression",children:"About delta compression"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"delta frames",src:t(44923).Z+"",width:"4002",height:"902"})}),"\n",(0,s.jsxs)(n.p,{children:["For a good overview of delta compression topic for the real-time messaging applications I suggest starting with a ",(0,s.jsx)(n.a,{href:"https://ably.com/blog/message-delta-compression",children:"blog post in Ably engineeiring blog"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://fossil-scm.org/home/doc/tip/www/delta_format.wiki",children:"Fossil"})," delta algorithm in Centrifugo instead of VCDIFF. The reason over VCDIFF was mainly two factors:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["availability of several Fossil delta implementations, specifically there are good libraries for Go (see ",(0,s.jsx)(n.a,{href:"https://github.com/shadowspore/fossil-delta",children:"shadowspore/fossil-delta"}),"), and for Javascript - ",(0,s.jsx)(n.a,{href:"https://github.com/dchest/fossil-delta-js",children:"fossil-delta-js"}),"."]}),"\n",(0,s.jsx)(n.li,{children:"the compactness of the algorithm implementation \u2013 under 500 lines of code in JavaScript"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"The 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",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.h2,{id:"experiment-overview",children:"Experiment Overview"}),"\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/el_classico.mp4"}),"\n",(0,s.jsx)(n.p,{children:"It visualizes only the score, but under the hood there are other game changes happen \u2013 will be shown below."}),"\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://www.wireshark.org/",children:"WireShark"})," with the filter:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"tcp.srcport == 8000 && websocket\n"})}),"\n",(0,s.jsx)(n.p,{children:"This is how WebSocket packets look in Wireshark when applying a filter mentioned above:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"wireshark",src:t(44157).Z+"",width:"2984",height:"1362"})}),"\n",(0,s.jsx)(n.p,{children:"Bytes captured show the entire overhead from packets in the game channel going from server to client (including TCP/IP overhead)."}),"\n",(0,s.jsxs)(n.p,{children:["The source code of the experiment may be found on Github as a ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/tree/master/_examples/compression_playground",children:"Centrifuge library example"}),". You can run it to inspect the exact WebSocket frames in each scenario."]}),"\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the data"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\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",(0,s.jsx)(n.p,{children:"Then 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",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the data"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\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",(0,s.jsx)(n.p,{children:"When we used Protobuf encoding for game state we serialized the data according to this Protobuf schema:"}),"\n",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the Protobuf schema for the game state"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-protobuf",children:'syntax = "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",(0,s.jsx)(n.h2,{id:"results-breakdown",children:"Results Breakdown"}),"\n",(0,s.jsx)(n.p,{children:"Below are the results of our experiment, comparing different protocols and compression settings:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Protocol"}),(0,s.jsx)(n.th,{children:"Compression"}),(0,s.jsx)(n.th,{children:"Delta"}),(0,s.jsx)(n.th,{children:"Bytes sent"}),(0,s.jsx)(n.th,{children:"Percentage"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"40251"}),(0,s.jsx)(n.td,{children:"100.0 (base)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"15669"}),(0,s.jsx)(n.td,{children:"38.93"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"6043"}),(0,s.jsx)(n.td,{children:"15.01"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"5360"}),(0,s.jsx)(n.td,{children:"13.32"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"39180"}),(0,s.jsx)(n.td,{children:"97.34"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"15542"}),(0,s.jsx)(n.td,{children:"38.61"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4287"}),(0,s.jsx)(n.td,{children:"10.65"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4126"}),(0,s.jsx)(n.td,{children:"10.25"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"16562"}),(0,s.jsx)(n.td,{children:"41.15"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"13115"}),(0,s.jsx)(n.td,{children:"32.58"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4382"}),(0,s.jsx)(n.td,{children:"10.89"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4473"}),(0,s.jsx)(n.td,{children:"11.11"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"results-analysis",children:"Results analysis"}),"\n",(0,s.jsx)(n.p,{children:"Let's now discuss the results we observed in detail."}),"\n",(0,s.jsx)(n.h3,{id:"json-over-json",children:"JSON over JSON"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending JSON data with football match game state over JSON Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"/docs/transports/websocket#websocket_compression",children:"how to configure compression"})," in Centrifugo, note that it comes with CPU and memory overhead which depends on your load profile."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"/docs/server/delta_compression",children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsx)(n.h3,{id:"json-over-protobuf",children:"JSON over Protobuf"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending JSON data with football match game state over Protobuf Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{start:"5",children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"I guess at this point you may be curious how delta frames look like in case of JSON protocol. Here is a screenshot:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"delta frames",src:t(87936).Z+"",width:"2828",height:"572"})}),"\n",(0,s.jsxs)(n.ol,{start:"8",children:["\n",(0,s.jsx)(n.li,{children:"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",(0,s.jsx)(n.h3,{id:"protobuf-over-protobuf",children:"Protobuf over Protobuf"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending Protobuf binary data with football match game state over Protobuf Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{start:"9",children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Protobuf over Protobuf (No Compression, No Delta)\nBytes Sent: 16562\nPercentage: 41.15%\nAnalysis: Using Protobuf for both encoding and transmission ",(0,s.jsx)(n.strong,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.p,{children:"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 \u2013 you can achieve even greater savings if you have larger messages that are very similar to each other."})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},44157:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/compression_wireshark-b44531ca65e851165c62331d10d661f4.png"},44923:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png"},87936:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/delta_frames-7d915a6b62f3cbcbfa4e0a1d738e79df.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var s=t(67294);const i={},o=s.createContext(i);function r(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5077],{87210:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>d});var s=t(85893),i=t(11151);const o={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:!1},r=void 0,a={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:!1,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:!1},unlisted:!1,prevItem:{title:"Proper real-time document state synchronization within Centrifugal ecosystem",permalink:"/blog/2024/06/03/real-time-document-state-sync"},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"}},l={authorsImageUrls:[void 0]},d=[{value:"About delta compression",id:"about-delta-compression",level:2},{value:"Experiment Overview",id:"experiment-overview",level:2},{value:"Results Breakdown",id:"results-breakdown",level:2},{value:"Results analysis",id:"results-analysis",level:2},{value:"JSON over JSON",id:"json-over-json",level:3},{value:"JSON over Protobuf",id:"json-over-protobuf",level:3},{value:"Protobuf over Protobuf",id:"protobuf-over-protobuf",level:3},{value:"Conclusion",id:"conclusion",level:2}];function c(e){const n={a:"a",code:"code",h2:"h2",h3:"h3",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components},{Details:o}=n;return o||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)("img",{src:"/img/football_match_compression.jpg"}),"\n",(0,s.jsx)(n.p,{children:"Optimizing 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",(0,s.jsxs)(n.p,{children:["Centrifugo v5.4.0 introduced ",(0,s.jsx)(n.a,{href:"/docs/server/delta_compression",children:"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",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.h2,{id:"about-delta-compression",children:"About delta compression"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"delta frames",src:t(85259).Z+"",width:"4002",height:"902"})}),"\n",(0,s.jsxs)(n.p,{children:["For a good overview of delta compression topic for the real-time messaging applications I suggest starting with a ",(0,s.jsx)(n.a,{href:"https://ably.com/blog/message-delta-compression",children:"blog post in Ably engineeiring blog"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://fossil-scm.org/home/doc/tip/www/delta_format.wiki",children:"Fossil"})," delta algorithm in Centrifugo instead of VCDIFF. The reason over VCDIFF was mainly two factors:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["availability of several Fossil delta implementations, specifically there are good libraries for Go (see ",(0,s.jsx)(n.a,{href:"https://github.com/shadowspore/fossil-delta",children:"shadowspore/fossil-delta"}),"), and for Javascript - ",(0,s.jsx)(n.a,{href:"https://github.com/dchest/fossil-delta-js",children:"fossil-delta-js"}),"."]}),"\n",(0,s.jsx)(n.li,{children:"the compactness of the algorithm implementation \u2013 under 500 lines of code in JavaScript"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"The 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",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.h2,{id:"experiment-overview",children:"Experiment Overview"}),"\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/el_classico.mp4"}),"\n",(0,s.jsx)(n.p,{children:"It visualizes only the score, but under the hood there are other game changes happen \u2013 will be shown below."}),"\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"https://www.wireshark.org/",children:"WireShark"})," with the filter:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"tcp.srcport == 8000 && websocket\n"})}),"\n",(0,s.jsx)(n.p,{children:"This is how WebSocket packets look in Wireshark when applying a filter mentioned above:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"wireshark",src:t(42968).Z+"",width:"2984",height:"1362"})}),"\n",(0,s.jsx)(n.p,{children:"Bytes captured show the entire overhead from packets in the game channel going from server to client (including TCP/IP overhead)."}),"\n",(0,s.jsxs)(n.p,{children:["The source code of the experiment may be found on Github as a ",(0,s.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/tree/master/_examples/compression_playground",children:"Centrifuge library example"}),". You can run it to inspect the exact WebSocket frames in each scenario."]}),"\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the data"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\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",(0,s.jsx)(n.p,{children:"Then 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",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the data"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\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",(0,s.jsx)(n.p,{children:"When we used Protobuf encoding for game state we serialized the data according to this Protobuf schema:"}),"\n",(0,s.jsxs)(o,{children:[(0,s.jsx)("summary",{children:"Click to see the Protobuf schema for the game state"}),(0,s.jsx)("p",{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-protobuf",children:'syntax = "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",(0,s.jsx)(n.h2,{id:"results-breakdown",children:"Results Breakdown"}),"\n",(0,s.jsx)(n.p,{children:"Below are the results of our experiment, comparing different protocols and compression settings:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Protocol"}),(0,s.jsx)(n.th,{children:"Compression"}),(0,s.jsx)(n.th,{children:"Delta"}),(0,s.jsx)(n.th,{children:"Bytes sent"}),(0,s.jsx)(n.th,{children:"Percentage"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"40251"}),(0,s.jsx)(n.td,{children:"100.0 (base)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"15669"}),(0,s.jsx)(n.td,{children:"38.93"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"6043"}),(0,s.jsx)(n.td,{children:"15.01"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over JSON"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"5360"}),(0,s.jsx)(n.td,{children:"13.32"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"39180"}),(0,s.jsx)(n.td,{children:"97.34"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"15542"}),(0,s.jsx)(n.td,{children:"38.61"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4287"}),(0,s.jsx)(n.td,{children:"10.65"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"JSON over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4126"}),(0,s.jsx)(n.td,{children:"10.25"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"}),(0,s.jsx)(n.td,{children:"--"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"16562"}),(0,s.jsx)(n.td,{children:"41.15"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"13115"}),(0,s.jsx)(n.td,{children:"32.58"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"No"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4382"}),(0,s.jsx)(n.td,{children:"10.89"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Protobuf over Protobuf"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"Yes"}),(0,s.jsx)(n.td,{children:"4473"}),(0,s.jsx)(n.td,{children:"11.11"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"results-analysis",children:"Results analysis"}),"\n",(0,s.jsx)(n.p,{children:"Let's now discuss the results we observed in detail."}),"\n",(0,s.jsx)(n.h3,{id:"json-over-json",children:"JSON over JSON"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending JSON data with football match game state over JSON Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"/docs/transports/websocket#websocket_compression",children:"how to configure compression"})," in Centrifugo, note that it comes with CPU and memory overhead which depends on your load profile."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["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 ",(0,s.jsx)(n.a,{href:"/docs/server/delta_compression",children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsx)(n.h3,{id:"json-over-protobuf",children:"JSON over Protobuf"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending JSON data with football match game state over Protobuf Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{start:"5",children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"I guess at this point you may be curious how delta frames look like in case of JSON protocol. Here is a screenshot:"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"delta frames",src:t(9857).Z+"",width:"2828",height:"572"})}),"\n",(0,s.jsxs)(n.ol,{start:"8",children:["\n",(0,s.jsx)(n.li,{children:"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",(0,s.jsx)(n.h3,{id:"protobuf-over-protobuf",children:"Protobuf over Protobuf"}),"\n",(0,s.jsx)(n.p,{children:"In this case we are sending Protobuf binary data with football match game state over Protobuf Centrifugal protocol."}),"\n",(0,s.jsxs)(n.ol,{start:"9",children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Protobuf over Protobuf (No Compression, No Delta)\nBytes Sent: 16562\nPercentage: 41.15%\nAnalysis: Using Protobuf for both encoding and transmission ",(0,s.jsx)(n.strong,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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"]}),"\n",(0,s.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"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",(0,s.jsx)(n.p,{children:"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 \u2013 you can achieve even greater savings if you have larger messages that are very similar to each other."})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},42968:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/compression_wireshark-b44531ca65e851165c62331d10d661f4.png"},85259:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png"},9857:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/delta_frames-7d915a6b62f3cbcbfa4e0a1d738e79df.png"},11151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>r});var s=t(67294);const i={},o=s.createContext(i);function r(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3c51ccb2.57855bec.js b/assets/js/3c51ccb2.a857b5f6.js similarity index 99% rename from assets/js/3c51ccb2.57855bec.js rename to assets/js/3c51ccb2.a857b5f6.js index 8b6d1e9fa..6c33d4f30 100644 --- a/assets/js/3c51ccb2.57855bec.js +++ b/assets/js/3c51ccb2.a857b5f6.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4160],{82639:(e,i,s)=>{s.r(i),s.d(i,{assets:()=>l,contentTitle:()=>r,default:()=>a,frontMatter:()=>n,metadata:()=>c,toc:()=>o});var t=s(85893),d=s(11151);const n={id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},r=void 0,c={id:"pro/push_notifications",title:"Push notification API",description:"This PRO feature is under active development, some changes expected here \ud83d\udea7",source:"@site/versioned_docs/version-4/pro/push_notifications.md",sourceDirName:"pro",slug:"/pro/push_notifications",permalink:"/docs/4/pro/push_notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/push_notifications.md",tags:[],version:"4",frontMatter:{id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},sidebar:"Pro",previous:{title:"Operation throttling",permalink:"/docs/4/pro/throttling"},next:{title:"User status API",permalink:"/docs/4/pro/user_status"}},l={},o=[{value:"Motivation and design choices",id:"motivation-and-design-choices",level:2},{value:"Storage for tokens",id:"storage-for-tokens",level:3},{value:"Efficient queuing",id:"efficient-queuing",level:3},{value:"Unified secure topics",id:"unified-secure-topics",level:3},{value:"Non-obtrusive proxying",id:"non-obtrusive-proxying",level:3},{value:"Builtin analytics",id:"builtin-analytics",level:3},{value:"Steps to integrate",id:"steps-to-integrate",level:2},{value:"Configuration",id:"configuration",level:2},{value:"FCM",id:"fcm",level:3},{value:"HMS",id:"hms",level:3},{value:"APNs",id:"apns",level:3},{value:"Other options",id:"other-options",level:3},{value:"push_notifications.max_inactive_device_days",id:"push_notificationsmax_inactive_device_days",level:4},{value:"Use PostgreSQL as queue",id:"use-postgresql-as-queue",level:3},{value:"API description",id:"api-description",level:2},{value:"device_register",id:"device_register",level:3},{value:"device_register request",id:"device_register-request",level:4},{value:"device_register result",id:"device_register-result",level:4},{value:"device_update",id:"device_update",level:3},{value:"device_update request",id:"device_update-request",level:4},{value:"device_update result",id:"device_update-result",level:4},{value:"device_remove",id:"device_remove",level:3},{value:"device_remove request",id:"device_remove-request",level:4},{value:"device_remove result",id:"device_remove-result",level:4},{value:"device_list",id:"device_list",level:3},{value:"device_list request",id:"device_list-request",level:4},{value:"device_list result",id:"device_list-result",level:4},{value:"device_topic_update",id:"device_topic_update",level:3},{value:"device_topic_update request",id:"device_topic_update-request",level:4},{value:"device_topic_update result",id:"device_topic_update-result",level:4},{value:"device_topic_list",id:"device_topic_list",level:3},{value:"device_topic_list request",id:"device_topic_list-request",level:4},{value:"device_topic_list result",id:"device_topic_list-result",level:4},{value:"user_topic_update",id:"user_topic_update",level:3},{value:"user_topic_update request",id:"user_topic_update-request",level:4},{value:"user_topic_update result",id:"user_topic_update-result",level:4},{value:"user_topic_list",id:"user_topic_list",level:3},{value:"user_topic_list request",id:"user_topic_list-request",level:4},{value:"user_topic_list result",id:"user_topic_list-result",level:4},{value:"send_push_notification",id:"send_push_notification",level:3},{value:"send_push_notification request",id:"send_push_notification-request",level:4},{value:"send_push_notification result",id:"send_push_notification-result",level:4},{value:"update_push_status",id:"update_push_status",level:3},{value:"update_push_status request",id:"update_push_status-request",level:4},{value:"update_push_status result",id:"update_push_status-result",level:4},{value:"Metrics",id:"metrics",level:2},{value:"Further reading and tutorials",id:"further-reading-and-tutorials",level:2}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.p,{children:"This PRO feature is under active development, some changes expected here \ud83d\udea7"}),"\n",(0,t.jsx)(i.p,{children:"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."}),"\n",(0,t.jsx)(i.p,{children:"With Centrifugo PRO push notifications may be delivered to all popular application platforms:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," Android devices"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," iOS devices"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})," Web browsers which support Web Push API (Chrome, Firefox, see ",(0,t.jsx)("a",{href:"https://caniuse.com/push-api",children:"this matrix"}),")"]}),"\n"]}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO provides API to manage user device tokens, device topic subscriptions and API to send push notifications towards registered devices and group of devices (subscribed to a topic)."}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Push",src:s(15085).Z+"",width:"2879",height:"1195"})}),"\n",(0,t.jsx)(i.p,{children:"To deliver push notifications to devices Centrifugo PRO integrates with the following providers:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging",children:"Firebase Cloud Messaging (FCM)"})," ",(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," ",(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/hms/huawei-pushkit/",children:"Huawei Messaging Service (HMS) Push Kit"})," ",(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," ",(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications",children:"Apple Push Notification service (APNs) "})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})]}),"\n"]}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO provides a comprehensive solution for sending push notifications by incorporating frontend SDKs from FCM, HMS, and Apple SDKs."}),"\n",(0,t.jsx)(i.p,{children:"While these push notification providers handle the frontend and transport aspects of notification delivery, device token management and efficient push notification broadcasting still need to be addressed by the application backend. Centrifugo PRO offers an API for storing tokens in a PostgreSQL database and managing device subscriptions to topics in a secure, unified manner."}),"\n",(0,t.jsx)(i.p,{children:"To facilitate efficient push notification broadcasting towards devices, Centrifugo PRO includes worker queues based on Redis streams."}),"\n",(0,t.jsx)(i.p,{children:"Integration with FCM means that you can use existing Firebase messaging SDKs to extract push notification token for a device on different platforms (iOS, Android, Flutter, web browser) and setting up push notification listeners. The same for HMS and APNs - just use existing native SDKs and best practices on the frontend. Only a couple of additional steps required to integrate frontend with Centrifugo PRO device token and device topic storage. After doing that you will be able to send push notification towards single device, or towards group of devices subscribed to a topic. For example, with a simple Centrifugo API call like this:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-bash",children:'curl -X POST http://localhost:8000/api \\\n-H "Authorization: apikey <KEY>" \\\n-d @- <<\'EOF\'\n\n{\n "method": "send_push_notification",\n "params": {\n "recipient": {"topics": ["test"]},\n "notification": {\n "fcm": {\n "message": {\n "notification": {"title": "Hello", "body": "How are you?"}\n }\n }\n }\n }\n}\nEOF\n'})}),"\n",(0,t.jsx)(i.h2,{id:"motivation-and-design-choices",children:"Motivation and design choices"}),"\n",(0,t.jsx)(i.p,{children:"We tried to be practical with our Push Notification API, let's look at its design choices and implementation properties we were able to achieve."}),"\n",(0,t.jsx)(i.h3,{id:"storage-for-tokens",children:"Storage for tokens"}),"\n",(0,t.jsx)(i.p,{children:"To start delivering push notifications in the application, developers usually need to integrate with providers such as FCM, HMS, and APNs. This integration typically requires the storage of device tokens in the application database and the implementation of sending push messages to provider push services."}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO simplifies the process by providing a backend for device token storage, following best practices in token management. It reacts to errors and periodically removes stale devices/tokens to maintain a working set of device tokens based on provider recommendations."}),"\n",(0,t.jsx)(i.h3,{id:"efficient-queuing",children:"Efficient queuing"}),"\n",(0,t.jsx)(i.p,{children:"Additionally, Centrifugo PRO provides an efficient, scalable queuing mechanism for sending push notifications. Developers can send notifications from the app backend to Centrifugo API with minimal latency and let Centrifugo process sending to FCM, HMS, APNs concurrently using built-in workers. In our tests, we achieved hundreds of thousands of pushes in tens of seconds."}),"\n",(0,t.jsx)(i.h3,{id:"unified-secure-topics",children:"Unified secure topics"}),"\n",(0,t.jsxs)(i.p,{children:["FCM and HMS have a built-in way of sending notification to large groups of devices over ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/android/topic-messaging",children:"topics"})," mechanism (",(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/doc/development/HMS-Plugin-Guides-V1/subscribetopic-0000001056797545-V1",children:"the same for HMS"}),"). One problem with native FCM or HMS topics though is that client can subscribe to any topic from the frontend side without any permission check. In today's world this is usually not desired. So Centrifugo PRO re-implements FCM, HMS topics by introducing an additional API to manage device subscriptions to topics."]}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"In some cases you may have real-time channels and device subscription topics with matching names \u2013 to send messages to both online and offline users. Though it's up to you."})}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO device topic subscriptions also add a way to introduce the missing topic semantics for APNs."}),"\n",(0,t.jsxs)(i.p,{children:["Centrifugo PRO additionally provides an API to create persistent bindings of user to notification topics. Then \u2013 as soon as user registers a device \u2013 it will be automatically subscribed to its own topics. As soon as user logs out from the app and you update user ID of the device - user topics binded to the device automatically removed/switched. This design solves one of the issues with FCM \u2013 if two different users use the same device it's becoming problematic to unsubscribe the device from large number of topics upon logout. Also, as soon as user to topic binding added (using ",(0,t.jsx)(i.code,{children:"user_topic_update"})," API) \u2013 it will be synchronized across all user active devices. You can still manage such persistent subscriptions on the application backend side if you prefer and provide the full list inside ",(0,t.jsx)(i.code,{children:"device_register"})," call."]}),"\n",(0,t.jsx)(i.h3,{id:"non-obtrusive-proxying",children:"Non-obtrusive proxying"}),"\n",(0,t.jsx)(i.p,{children:"Unlike other solutions that combine different provider push sending APIs into a unified API, Centrifugo PRO provides a non-obtrusive proxy for all the mentioned providers. Developers can send notification payloads in a format defined by each provider."}),"\n",(0,t.jsx)(i.p,{children:"It's also possible to send notifications into native FCM, HMS topics or send to raw FCM, HMS, APNs tokens using Centrifugo PRO's push API, allowing them to combine native provider primitives with those added by Centrifugo (i.e., sending to a list of device IDs or to a list of topics)."}),"\n",(0,t.jsx)(i.h3,{id:"builtin-analytics",children:"Builtin analytics"}),"\n",(0,t.jsxs)(i.p,{children:["Furthermore, Centrifugo PRO offers the ability to inspect sent push notifications using ",(0,t.jsx)(i.a,{href:"/docs/4/pro/analytics#notifications-table",children:"ClickHouse analytics"}),". Providers may also offer their own analytics, ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/understand-delivery?platform=web",children:"such as FCM"}),", which provides insight into push notification delivery. Centrifugo PRO also offers a way to analyze push notification delivery and interaction using the ",(0,t.jsx)(i.code,{children:"update_push_status"})," API."]}),"\n",(0,t.jsx)(i.h2,{id:"steps-to-integrate",children:"Steps to integrate"}),"\n",(0,t.jsxs)(i.ol,{children:["\n",(0,t.jsxs)(i.li,{children:["Add provider SDK on the frontend side, follow provider instructions for your platform to obtain a push token for a device. For example, for FCM see instructions for ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/ios/client",children:"iOS"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/android/client",children:"Android"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/flutter/client",children:"Flutter"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/js/client",children:"Web Browser"}),"). The same for HMS or APNs \u2013 frontend part should be handled by their native SDKs."]}),"\n",(0,t.jsxs)(i.li,{children:["Call Centrifugo PRO backend API with the obtained token. From the application backend call Centrifugo ",(0,t.jsx)(i.code,{children:"device_register"})," API to register the device in Centrifugo PRO storage. Optionally provide list of topics to subscribe device to."]}),"\n",(0,t.jsx)(i.li,{children:"Centrifugo returns a registered device object. Pass a generated device ID to the frontend and save it on the frontend together with a token received from FCM."}),"\n",(0,t.jsxs)(i.li,{children:["Call Centrifugo ",(0,t.jsx)(i.code,{children:"send_push_notification"})," API whenever it's time to deliver a push notification."]}),"\n"]}),"\n",(0,t.jsxs)(i.p,{children:["At any moment you can inspect device storage by calling ",(0,t.jsx)(i.code,{children:"device_list"})," API."]}),"\n",(0,t.jsxs)(i.p,{children:["Once user logs out from the app, you can detach user ID from device by using ",(0,t.jsx)(i.code,{children:"device_update"})," or remove device with ",(0,t.jsx)(i.code,{children:"device_remove"})," API."]}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(i.p,{children:"In Centrifugo PRO you can configure one push provider or use all of them \u2013 this choice is up to you."}),"\n",(0,t.jsx)(i.h3,{id:"fcm",children:"FCM"}),"\n",(0,t.jsxs)(i.p,{children:["As mentioned above Centrifigo uses PostgreSQL for token storage. To enable push notifications make sure ",(0,t.jsx)(i.code,{children:"database"})," section defined in the configration and ",(0,t.jsx)(i.code,{children:"fcm"})," is in the ",(0,t.jsx)(i.code,{children:"push_notifications.enabled_providers"})," list. Centrifugo PRO uses Redis for queuing push notification requests, so Redis address should be configured also. Finally, to integrate with FCM a path to the credentials file must be provided (see how to create one ",(0,t.jsx)(i.a,{href:"https://github.com/Catapush/catapush-docs/blob/master/AndroidSDK/DOCUMENTATION_PLATFORM_GMS_FCM.md",children:"in this instruction"}),"). So the full configuration to start sending push notifications over FCM may look like this:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["fcm"],\n "fcm_credentials_file_path": "/path/to/service/account/credentials.json"\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"Actually, PostgreSQL database configuration is optional here \u2013 you can use push notifications API without it. In this case you will be able to send notifications to FCM, HMS, APNs raw tokens, FCM and HMS native topics and conditions. I.e. using Centrifugo as an efficient proxy for push notifications (for example if you already keep tokens in your database). But sending to device ids and topics, and token/topic management APIs won't be available for usage."})}),"\n",(0,t.jsx)(i.h3,{id:"hms",children:"HMS"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["hms"],\n "hms_app_id": "<your_app_id>",\n "hms_app_secret": "<your_app_secret>",\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsxs)(i.p,{children:["See example how to get app id and app secret ",(0,t.jsx)(i.a,{href:"https://github.com/Catapush/catapush-docs/blob/master/AndroidSDK/DOCUMENTATION_PLATFORM_HMS_PUSHKIT.md",children:"here"}),"."]})}),"\n",(0,t.jsx)(i.h3,{id:"apns",children:"APNs"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["apns"],\n "apns_endpoint": "development",\n "apns_bundle_id": "com.example.your_app",\n "apns_auth": "token",\n "apns_token_auth_key_path": "/path/to/auth/key/file.p8",\n "apns_token_key_id": "<your_key_id>",\n "apns_token_team_id": "your_team_id",\n }\n}\n'})}),"\n",(0,t.jsx)(i.p,{children:"We also support auth over p12 certificates with the following options:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_path"})}),"\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_b64"})}),"\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_password"})}),"\n"]}),"\n",(0,t.jsx)(i.h3,{id:"other-options",children:"Other options"}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsmax_inactive_device_days",children:"push_notifications.max_inactive_device_days"}),"\n",(0,t.jsx)(i.p,{children:"This option configures the number of days to keep device without updates. By default Centrifugo does not remove inactive devices."}),"\n",(0,t.jsx)(i.h3,{id:"use-postgresql-as-queue",children:"Use PostgreSQL as queue"}),"\n",(0,t.jsx)(i.p,{children:"Coming soon \ud83d\udea7"}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO utilizes Redis Streams as the default queue engine for push notifications. However, it also offers the option to employ PostgreSQL for queuing. It's as simple as:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "queue_engine": "database",\n // rest of the options...\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"Queue based on Redis streams is faster, so if you start with PostgreSQL based queue \u2013 you have an option to switch to a more performant implementation later. Though active push notifications will be lost during a switch."})}),"\n",(0,t.jsx)(i.h2,{id:"api-description",children:"API description"}),"\n",(0,t.jsx)(i.h3,{id:"device_register",children:"device_register"}),"\n",(0,t.jsx)(i.p,{children:"Registers or updates device information."}),"\n",(0,t.jsx)(i.h4,{id:"device_register-request",children:"device_register request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"ID of the device being registered (provide it when updating)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Provider of the device token (valid choices: ",(0,t.jsx)(i.code,{children:"fcm"}),", ",(0,t.jsx)(i.code,{children:"hms"}),", ",(0,t.jsx)(i.code,{children:"apns"}),")."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"token"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Push notification token for the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platform"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Platform of the device (valid choices: ",(0,t.jsx)(i.code,{children:"ios"}),", ",(0,t.jsx)(i.code,{children:"android"}),", ",(0,t.jsx)(i.code,{children:"web"}),")."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"User associated with the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"array of strings"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Device topic subscriptions. This should be a full list which replaces all the topics previously accociated with the device. User topics managed by ",(0,t.jsx)(i.code,{children:"UserTopic"})," model will be automatically attached."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional tags for the device (indexed key-value data)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional metadata for the device (not indexed)."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_register-result",children:"device_register result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"The device ID that was registered/updated."})]})})]}),"\n",(0,t.jsx)(i.h3,{id:"device_update",children:"device_update"}),"\n",(0,t.jsx)(i.p,{children:"Call this method to update device. For example, when user logs out the app and you need to detach user ID from the device."}),"\n",(0,t.jsx)(i.h4,{id:"device_update-request",children:"device_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Device ids to filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Device users filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsx)(i.td,{children:"repeated DeviceProviderTokens"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user_update"})}),(0,t.jsx)(i.td,{children:"DeviceUserUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional user update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta_update"})}),(0,t.jsx)(i.td,{children:"DeviceMetaUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional device meta update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags_update"})}),(0,t.jsx)(i.td,{children:"DeviceTagsUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional device tags update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics_update"})}),(0,t.jsx)(i.td,{children:"DeviceChannelsUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional topics update object"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceUserUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceMetaUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Meta to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTagsUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Tags to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceChannelsUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channels to set"})]})})]}),"\n",(0,t.jsx)(i.h4,{id:"device_update-result",children:"device_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_remove",children:"device_remove"}),"\n",(0,t.jsx)(i.p,{children:"Removes device from storage. This may be also called when user logs out the app and you don't need its device token after that."}),"\n",(0,t.jsx)(i.h4,{id:"device_remove-request",children:"device_remove request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"A list of device IDs to be removed"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"A list of device user IDs to filter devices to remove"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ProviderTokens"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to remove"})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_remove-result",children:"device_remove result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_list",children:"device_list"}),"\n",(0,t.jsx)(i.p,{children:"Returns a paginated list of registered devices according to request filter conditions."}),"\n",(0,t.jsx)(i.h4,{id:"device_list-request",children:"device_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device IDs to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"providers"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device token providers to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"ProviderTokens"})]}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platforms"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device platforms to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last device id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Maximum number of devices to retrieve."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_topics"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include topics information for each device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_tags"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include tags information for each device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_meta"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include meta information for each device."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_list-result",children:"device_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"Device"})]}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A list of devices"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"Device"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's token provider."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"token"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's token."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platform"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's platform."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The user associated with the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"array of strings"}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_topics"})," was true"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_tags"})," was true"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_meta"})," was true"]})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"device_topic_update",children:"device_topic_update"}),"\n",(0,t.jsx)(i.p,{children:"Manage mapping of device to topics."}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_update-request",children:"device_topic_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"add"})," or ",(0,t.jsx)(i.code,{children:"remove"})," or ",(0,t.jsx)(i.code,{children:"set"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_update-result",children:"device_topic_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_topic_list",children:"device_topic_list"}),"\n",(0,t.jsx)(i.p,{children:"List device to topic mapping."}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_list-request",children:"device_topic_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device IDs to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_providers"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device token providers to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_provider_tokens"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"ProviderTokens"})]}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_platforms"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device platforms to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic_prefix"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Channel prefix to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last device id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Maximum number of devices to retrieve."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_device"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include Device information for each object."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_list-result",children:"device_topic_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"DeviceChannel"})]}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A list of DeviceChannel objects"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceChannel"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"ID of DeviceChannel"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channel"})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"user_topic_update",children:"user_topic_update"}),"\n",(0,t.jsx)(i.p,{children:"Manage mapping of topics with users. These user topics will be automatically attached to user devices upon registering. And removed from device upon deattaching user."}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_update-request",children:"user_topic_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"add"})," or ",(0,t.jsx)(i.code,{children:"remove"})," or ",(0,t.jsx)(i.code,{children:"set"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_update-result",children:"user_topic_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"user_topic_list",children:"user_topic_list"}),"\n",(0,t.jsx)(i.p,{children:"List user to topic mapping."}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_list-request",children:"user_topic_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic_prefix"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Channel prefix to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Maximum number of ",(0,t.jsx)(i.code,{children:"UserTopic"})," objects to retrieve."]})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_list-result",children:"user_topic_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"UserTopic"})]}),(0,t.jsx)(i.td,{children:"A list of UserTopic objects"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"UserTopic"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["ID of ",(0,t.jsx)(i.code,{children:"UserTopic"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channel"})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"send_push_notification",children:"send_push_notification"}),"\n",(0,t.jsxs)(i.p,{children:["Send push notification to specific ",(0,t.jsx)(i.code,{children:"device_ids"}),", or to ",(0,t.jsx)(i.code,{children:"topics"}),", or native provider identifiers like ",(0,t.jsx)(i.code,{children:"fcm_tokens"}),", or to ",(0,t.jsx)(i.code,{children:"fcm_topic"}),". Request will be queued by Centrifugo, consumed by Centrifugo built-in workers and sent to the provider API."]}),"\n",(0,t.jsx)(i.h4,{id:"send_push_notification-request",children:"send_push_notification request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"recipient"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"PushRecipient"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Recipient of push notification"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"notification"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"PushNotification"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Push notification to send"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"PushRecipient"})," (you ",(0,t.jsx)(i.strong,{children:"must set only one of the following fields"}),"):"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of device IDs (managed by Centrifugo)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to topics (managed by Centrifugo)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of FCM native tokens"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a FCM native topic"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_condition"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a FCM native condition"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of HMS native tokens"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a HMS native topic"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_condition"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a HMS native condition"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"apns_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of APNs native tokens"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"PushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Unique send id, used for Centrifugo builtin analytics"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"expire_at"})}),(0,t.jsx)(i.td,{children:"int64"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Unix timestamp when Centrifugo stops attempting to send this notification (this does not relate to notification TTL fields)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"FcmPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for FCM"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"HmsPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for HMS"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"apns"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ApnsPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for APNs"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"FcmPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"message"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["FCM ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Message",children:"Message"})," described in FCM docs."]})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"HmsPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"message"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["HMS ",(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197#EN-US_TOPIC_0000001134031085__p1324218481619",children:"Message"})," described in HMS Push Kit docs."]})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"ApnsPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"headers"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["APNs ",(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns",children:"headers"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"payload"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["APNs ",(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification",children:"payload"})]})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"send_push_notification-result",children:"send_push_notification result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsxs)(i.td,{children:["Unique send id, matches ",(0,t.jsx)(i.code,{children:"uid"})," in request if it was provided"]})]})})]}),"\n",(0,t.jsx)(i.h3,{id:"update_push_status",children:"update_push_status"}),"\n",(0,t.jsx)(i.p,{children:"This API call is experimental, some changes may happen here."}),"\n",(0,t.jsxs)(i.p,{children:["Centrifugo PRO also allows tracking status of push notification delivery and interaction. It's possible to use ",(0,t.jsx)(i.code,{children:"update_push_status"})," API to save the updated status of push notification to the ",(0,t.jsx)(i.code,{children:"notifications"})," ",(0,t.jsx)(i.a,{href:"/docs/4/pro/analytics#notifications-table",children:"analytics table"}),". Then it's possible to build insights into push notification effectiveness by querying the table."]}),"\n",(0,t.jsxs)(i.p,{children:["The ",(0,t.jsx)(i.code,{children:"update_push_status"})," API supposes that you are using ",(0,t.jsx)(i.code,{children:"uid"})," field with each notification sent and you are using Centrifugo PRO generated device IDs (as described in ",(0,t.jsx)(i.a,{href:"#steps-to-integrate",children:"steps to integrate"}),")."]}),"\n",(0,t.jsx)(i.p,{children:"This is a part of server API at the moment, so you need to proxy requests to this endpoint over your backend. We can consider making this API suitable for requests from the client side \u2013 please reach out if your use case requires it."}),"\n",(0,t.jsx)(i.h4,{id:"update_push_status-request",children:"update_push_status request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"uid"})," (unique send id) from ",(0,t.jsx)(i.code,{children:"send_push_notification"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"status"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Status of push notification - ",(0,t.jsx)(i.code,{children:"delivered"})," or ",(0,t.jsx)(i.code,{children:"interacted"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"msg_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Message ID"})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"update_push_status-result",children:"update_push_status result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h2,{id:"metrics",children:"Metrics"}),"\n",(0,t.jsx)(i.p,{children:"Several metrics are available to monitor the state of Centrifugo push worker system:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_notification_count"})," - counter, shows total count of push notifications sent to providers (splitted by provider, recipient type, platform, success, error code)."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_queue_consuming_lag"})," - gauge, shows the lag of queues, should be close to zero most of the time. Splitted by provider and name of queue."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_consuming_inflight_jobs"})," - gauge, shows immediate number of workers proceccing pushes. Splitted by provider and name of queue."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_job_duration_seconds"})," - summary, provides insights about worker job duration timings. Splitted by provider and recipient type."]}),"\n"]}),"\n",(0,t.jsx)(i.h2,{id:"further-reading-and-tutorials",children:"Further reading and tutorials"}),"\n",(0,t.jsx)(i.p,{children:"Coming soon."})]})}function a(e={}){const{wrapper:i}={...(0,d.a)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},15085:(e,i,s)=>{s.d(i,{Z:()=>t});const t=s.p+"assets/images/push_notifications-c1af39fb6bbb1da727bd940368acd4f8.png"},11151:(e,i,s)=>{s.d(i,{Z:()=>c,a:()=>r});var t=s(67294);const d={},n=t.createContext(d);function r(e){const i=t.useContext(n);return t.useMemo((function(){return"function"==typeof e?e(i):{...i,...e}}),[i,e])}function c(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:r(e.components),t.createElement(n.Provider,{value:i},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4160],{82639:(e,i,s)=>{s.r(i),s.d(i,{assets:()=>l,contentTitle:()=>r,default:()=>a,frontMatter:()=>n,metadata:()=>c,toc:()=>o});var t=s(85893),d=s(11151);const n={id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},r=void 0,c={id:"pro/push_notifications",title:"Push notification API",description:"This PRO feature is under active development, some changes expected here \ud83d\udea7",source:"@site/versioned_docs/version-4/pro/push_notifications.md",sourceDirName:"pro",slug:"/pro/push_notifications",permalink:"/docs/4/pro/push_notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/push_notifications.md",tags:[],version:"4",frontMatter:{id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},sidebar:"Pro",previous:{title:"Operation throttling",permalink:"/docs/4/pro/throttling"},next:{title:"User status API",permalink:"/docs/4/pro/user_status"}},l={},o=[{value:"Motivation and design choices",id:"motivation-and-design-choices",level:2},{value:"Storage for tokens",id:"storage-for-tokens",level:3},{value:"Efficient queuing",id:"efficient-queuing",level:3},{value:"Unified secure topics",id:"unified-secure-topics",level:3},{value:"Non-obtrusive proxying",id:"non-obtrusive-proxying",level:3},{value:"Builtin analytics",id:"builtin-analytics",level:3},{value:"Steps to integrate",id:"steps-to-integrate",level:2},{value:"Configuration",id:"configuration",level:2},{value:"FCM",id:"fcm",level:3},{value:"HMS",id:"hms",level:3},{value:"APNs",id:"apns",level:3},{value:"Other options",id:"other-options",level:3},{value:"push_notifications.max_inactive_device_days",id:"push_notificationsmax_inactive_device_days",level:4},{value:"Use PostgreSQL as queue",id:"use-postgresql-as-queue",level:3},{value:"API description",id:"api-description",level:2},{value:"device_register",id:"device_register",level:3},{value:"device_register request",id:"device_register-request",level:4},{value:"device_register result",id:"device_register-result",level:4},{value:"device_update",id:"device_update",level:3},{value:"device_update request",id:"device_update-request",level:4},{value:"device_update result",id:"device_update-result",level:4},{value:"device_remove",id:"device_remove",level:3},{value:"device_remove request",id:"device_remove-request",level:4},{value:"device_remove result",id:"device_remove-result",level:4},{value:"device_list",id:"device_list",level:3},{value:"device_list request",id:"device_list-request",level:4},{value:"device_list result",id:"device_list-result",level:4},{value:"device_topic_update",id:"device_topic_update",level:3},{value:"device_topic_update request",id:"device_topic_update-request",level:4},{value:"device_topic_update result",id:"device_topic_update-result",level:4},{value:"device_topic_list",id:"device_topic_list",level:3},{value:"device_topic_list request",id:"device_topic_list-request",level:4},{value:"device_topic_list result",id:"device_topic_list-result",level:4},{value:"user_topic_update",id:"user_topic_update",level:3},{value:"user_topic_update request",id:"user_topic_update-request",level:4},{value:"user_topic_update result",id:"user_topic_update-result",level:4},{value:"user_topic_list",id:"user_topic_list",level:3},{value:"user_topic_list request",id:"user_topic_list-request",level:4},{value:"user_topic_list result",id:"user_topic_list-result",level:4},{value:"send_push_notification",id:"send_push_notification",level:3},{value:"send_push_notification request",id:"send_push_notification-request",level:4},{value:"send_push_notification result",id:"send_push_notification-result",level:4},{value:"update_push_status",id:"update_push_status",level:3},{value:"update_push_status request",id:"update_push_status-request",level:4},{value:"update_push_status result",id:"update_push_status-result",level:4},{value:"Metrics",id:"metrics",level:2},{value:"Further reading and tutorials",id:"further-reading-and-tutorials",level:2}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.p,{children:"This PRO feature is under active development, some changes expected here \ud83d\udea7"}),"\n",(0,t.jsx)(i.p,{children:"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."}),"\n",(0,t.jsx)(i.p,{children:"With Centrifugo PRO push notifications may be delivered to all popular application platforms:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," Android devices"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," iOS devices"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})," Web browsers which support Web Push API (Chrome, Firefox, see ",(0,t.jsx)("a",{href:"https://caniuse.com/push-api",children:"this matrix"}),")"]}),"\n"]}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO provides API to manage user device tokens, device topic subscriptions and API to send push notifications towards registered devices and group of devices (subscribed to a topic)."}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{alt:"Push",src:s(88404).Z+"",width:"2879",height:"1195"})}),"\n",(0,t.jsx)(i.p,{children:"To deliver push notifications to devices Centrifugo PRO integrates with the following providers:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging",children:"Firebase Cloud Messaging (FCM)"})," ",(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," ",(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/hms/huawei-pushkit/",children:"Huawei Messaging Service (HMS) Push Kit"})," ",(0,t.jsx)("i",{className:"bi bi-android2",style:{color:"yellowgreen"}})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})," ",(0,t.jsx)("i",{className:"bi bi-globe",style:{color:"orange"}})]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications",children:"Apple Push Notification service (APNs) "})," ",(0,t.jsx)("i",{className:"bi bi-apple",style:{color:"cornflowerblue"}})]}),"\n"]}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO provides a comprehensive solution for sending push notifications by incorporating frontend SDKs from FCM, HMS, and Apple SDKs."}),"\n",(0,t.jsx)(i.p,{children:"While these push notification providers handle the frontend and transport aspects of notification delivery, device token management and efficient push notification broadcasting still need to be addressed by the application backend. Centrifugo PRO offers an API for storing tokens in a PostgreSQL database and managing device subscriptions to topics in a secure, unified manner."}),"\n",(0,t.jsx)(i.p,{children:"To facilitate efficient push notification broadcasting towards devices, Centrifugo PRO includes worker queues based on Redis streams."}),"\n",(0,t.jsx)(i.p,{children:"Integration with FCM means that you can use existing Firebase messaging SDKs to extract push notification token for a device on different platforms (iOS, Android, Flutter, web browser) and setting up push notification listeners. The same for HMS and APNs - just use existing native SDKs and best practices on the frontend. Only a couple of additional steps required to integrate frontend with Centrifugo PRO device token and device topic storage. After doing that you will be able to send push notification towards single device, or towards group of devices subscribed to a topic. For example, with a simple Centrifugo API call like this:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-bash",children:'curl -X POST http://localhost:8000/api \\\n-H "Authorization: apikey <KEY>" \\\n-d @- <<\'EOF\'\n\n{\n "method": "send_push_notification",\n "params": {\n "recipient": {"topics": ["test"]},\n "notification": {\n "fcm": {\n "message": {\n "notification": {"title": "Hello", "body": "How are you?"}\n }\n }\n }\n }\n}\nEOF\n'})}),"\n",(0,t.jsx)(i.h2,{id:"motivation-and-design-choices",children:"Motivation and design choices"}),"\n",(0,t.jsx)(i.p,{children:"We tried to be practical with our Push Notification API, let's look at its design choices and implementation properties we were able to achieve."}),"\n",(0,t.jsx)(i.h3,{id:"storage-for-tokens",children:"Storage for tokens"}),"\n",(0,t.jsx)(i.p,{children:"To start delivering push notifications in the application, developers usually need to integrate with providers such as FCM, HMS, and APNs. This integration typically requires the storage of device tokens in the application database and the implementation of sending push messages to provider push services."}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO simplifies the process by providing a backend for device token storage, following best practices in token management. It reacts to errors and periodically removes stale devices/tokens to maintain a working set of device tokens based on provider recommendations."}),"\n",(0,t.jsx)(i.h3,{id:"efficient-queuing",children:"Efficient queuing"}),"\n",(0,t.jsx)(i.p,{children:"Additionally, Centrifugo PRO provides an efficient, scalable queuing mechanism for sending push notifications. Developers can send notifications from the app backend to Centrifugo API with minimal latency and let Centrifugo process sending to FCM, HMS, APNs concurrently using built-in workers. In our tests, we achieved hundreds of thousands of pushes in tens of seconds."}),"\n",(0,t.jsx)(i.h3,{id:"unified-secure-topics",children:"Unified secure topics"}),"\n",(0,t.jsxs)(i.p,{children:["FCM and HMS have a built-in way of sending notification to large groups of devices over ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/android/topic-messaging",children:"topics"})," mechanism (",(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/doc/development/HMS-Plugin-Guides-V1/subscribetopic-0000001056797545-V1",children:"the same for HMS"}),"). One problem with native FCM or HMS topics though is that client can subscribe to any topic from the frontend side without any permission check. In today's world this is usually not desired. So Centrifugo PRO re-implements FCM, HMS topics by introducing an additional API to manage device subscriptions to topics."]}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"In some cases you may have real-time channels and device subscription topics with matching names \u2013 to send messages to both online and offline users. Though it's up to you."})}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO device topic subscriptions also add a way to introduce the missing topic semantics for APNs."}),"\n",(0,t.jsxs)(i.p,{children:["Centrifugo PRO additionally provides an API to create persistent bindings of user to notification topics. Then \u2013 as soon as user registers a device \u2013 it will be automatically subscribed to its own topics. As soon as user logs out from the app and you update user ID of the device - user topics binded to the device automatically removed/switched. This design solves one of the issues with FCM \u2013 if two different users use the same device it's becoming problematic to unsubscribe the device from large number of topics upon logout. Also, as soon as user to topic binding added (using ",(0,t.jsx)(i.code,{children:"user_topic_update"})," API) \u2013 it will be synchronized across all user active devices. You can still manage such persistent subscriptions on the application backend side if you prefer and provide the full list inside ",(0,t.jsx)(i.code,{children:"device_register"})," call."]}),"\n",(0,t.jsx)(i.h3,{id:"non-obtrusive-proxying",children:"Non-obtrusive proxying"}),"\n",(0,t.jsx)(i.p,{children:"Unlike other solutions that combine different provider push sending APIs into a unified API, Centrifugo PRO provides a non-obtrusive proxy for all the mentioned providers. Developers can send notification payloads in a format defined by each provider."}),"\n",(0,t.jsx)(i.p,{children:"It's also possible to send notifications into native FCM, HMS topics or send to raw FCM, HMS, APNs tokens using Centrifugo PRO's push API, allowing them to combine native provider primitives with those added by Centrifugo (i.e., sending to a list of device IDs or to a list of topics)."}),"\n",(0,t.jsx)(i.h3,{id:"builtin-analytics",children:"Builtin analytics"}),"\n",(0,t.jsxs)(i.p,{children:["Furthermore, Centrifugo PRO offers the ability to inspect sent push notifications using ",(0,t.jsx)(i.a,{href:"/docs/4/pro/analytics#notifications-table",children:"ClickHouse analytics"}),". Providers may also offer their own analytics, ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/understand-delivery?platform=web",children:"such as FCM"}),", which provides insight into push notification delivery. Centrifugo PRO also offers a way to analyze push notification delivery and interaction using the ",(0,t.jsx)(i.code,{children:"update_push_status"})," API."]}),"\n",(0,t.jsx)(i.h2,{id:"steps-to-integrate",children:"Steps to integrate"}),"\n",(0,t.jsxs)(i.ol,{children:["\n",(0,t.jsxs)(i.li,{children:["Add provider SDK on the frontend side, follow provider instructions for your platform to obtain a push token for a device. For example, for FCM see instructions for ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/ios/client",children:"iOS"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/android/client",children:"Android"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/flutter/client",children:"Flutter"}),", ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/cloud-messaging/js/client",children:"Web Browser"}),"). The same for HMS or APNs \u2013 frontend part should be handled by their native SDKs."]}),"\n",(0,t.jsxs)(i.li,{children:["Call Centrifugo PRO backend API with the obtained token. From the application backend call Centrifugo ",(0,t.jsx)(i.code,{children:"device_register"})," API to register the device in Centrifugo PRO storage. Optionally provide list of topics to subscribe device to."]}),"\n",(0,t.jsx)(i.li,{children:"Centrifugo returns a registered device object. Pass a generated device ID to the frontend and save it on the frontend together with a token received from FCM."}),"\n",(0,t.jsxs)(i.li,{children:["Call Centrifugo ",(0,t.jsx)(i.code,{children:"send_push_notification"})," API whenever it's time to deliver a push notification."]}),"\n"]}),"\n",(0,t.jsxs)(i.p,{children:["At any moment you can inspect device storage by calling ",(0,t.jsx)(i.code,{children:"device_list"})," API."]}),"\n",(0,t.jsxs)(i.p,{children:["Once user logs out from the app, you can detach user ID from device by using ",(0,t.jsx)(i.code,{children:"device_update"})," or remove device with ",(0,t.jsx)(i.code,{children:"device_remove"})," API."]}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(i.p,{children:"In Centrifugo PRO you can configure one push provider or use all of them \u2013 this choice is up to you."}),"\n",(0,t.jsx)(i.h3,{id:"fcm",children:"FCM"}),"\n",(0,t.jsxs)(i.p,{children:["As mentioned above Centrifigo uses PostgreSQL for token storage. To enable push notifications make sure ",(0,t.jsx)(i.code,{children:"database"})," section defined in the configration and ",(0,t.jsx)(i.code,{children:"fcm"})," is in the ",(0,t.jsx)(i.code,{children:"push_notifications.enabled_providers"})," list. Centrifugo PRO uses Redis for queuing push notification requests, so Redis address should be configured also. Finally, to integrate with FCM a path to the credentials file must be provided (see how to create one ",(0,t.jsx)(i.a,{href:"https://github.com/Catapush/catapush-docs/blob/master/AndroidSDK/DOCUMENTATION_PLATFORM_GMS_FCM.md",children:"in this instruction"}),"). So the full configuration to start sending push notifications over FCM may look like this:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["fcm"],\n "fcm_credentials_file_path": "/path/to/service/account/credentials.json"\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"Actually, PostgreSQL database configuration is optional here \u2013 you can use push notifications API without it. In this case you will be able to send notifications to FCM, HMS, APNs raw tokens, FCM and HMS native topics and conditions. I.e. using Centrifugo as an efficient proxy for push notifications (for example if you already keep tokens in your database). But sending to device ids and topics, and token/topic management APIs won't be available for usage."})}),"\n",(0,t.jsx)(i.h3,{id:"hms",children:"HMS"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["hms"],\n "hms_app_id": "<your_app_id>",\n "hms_app_secret": "<your_app_secret>",\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsxs)(i.p,{children:["See example how to get app id and app secret ",(0,t.jsx)(i.a,{href:"https://github.com/Catapush/catapush-docs/blob/master/AndroidSDK/DOCUMENTATION_PLATFORM_HMS_PUSHKIT.md",children:"here"}),"."]})}),"\n",(0,t.jsx)(i.h3,{id:"apns",children:"APNs"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "redis_address": "localhost:6379",\n "enabled_providers": ["apns"],\n "apns_endpoint": "development",\n "apns_bundle_id": "com.example.your_app",\n "apns_auth": "token",\n "apns_token_auth_key_path": "/path/to/auth/key/file.p8",\n "apns_token_key_id": "<your_key_id>",\n "apns_token_team_id": "your_team_id",\n }\n}\n'})}),"\n",(0,t.jsx)(i.p,{children:"We also support auth over p12 certificates with the following options:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_path"})}),"\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_b64"})}),"\n",(0,t.jsx)(i.li,{children:(0,t.jsx)(i.code,{children:"push_notifications.apns_cert_p12_password"})}),"\n"]}),"\n",(0,t.jsx)(i.h3,{id:"other-options",children:"Other options"}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsmax_inactive_device_days",children:"push_notifications.max_inactive_device_days"}),"\n",(0,t.jsx)(i.p,{children:"This option configures the number of days to keep device without updates. By default Centrifugo does not remove inactive devices."}),"\n",(0,t.jsx)(i.h3,{id:"use-postgresql-as-queue",children:"Use PostgreSQL as queue"}),"\n",(0,t.jsx)(i.p,{children:"Coming soon \ud83d\udea7"}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO utilizes Redis Streams as the default queue engine for push notifications. However, it also offers the option to employ PostgreSQL for queuing. It's as simple as:"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "database": {\n "dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"\n },\n "push_notifications": {\n "queue_engine": "database",\n // rest of the options...\n }\n}\n'})}),"\n",(0,t.jsx)(i.admonition,{type:"tip",children:(0,t.jsx)(i.p,{children:"Queue based on Redis streams is faster, so if you start with PostgreSQL based queue \u2013 you have an option to switch to a more performant implementation later. Though active push notifications will be lost during a switch."})}),"\n",(0,t.jsx)(i.h2,{id:"api-description",children:"API description"}),"\n",(0,t.jsx)(i.h3,{id:"device_register",children:"device_register"}),"\n",(0,t.jsx)(i.p,{children:"Registers or updates device information."}),"\n",(0,t.jsx)(i.h4,{id:"device_register-request",children:"device_register request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"ID of the device being registered (provide it when updating)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Provider of the device token (valid choices: ",(0,t.jsx)(i.code,{children:"fcm"}),", ",(0,t.jsx)(i.code,{children:"hms"}),", ",(0,t.jsx)(i.code,{children:"apns"}),")."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"token"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Push notification token for the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platform"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Platform of the device (valid choices: ",(0,t.jsx)(i.code,{children:"ios"}),", ",(0,t.jsx)(i.code,{children:"android"}),", ",(0,t.jsx)(i.code,{children:"web"}),")."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"User associated with the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"array of strings"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Device topic subscriptions. This should be a full list which replaces all the topics previously accociated with the device. User topics managed by ",(0,t.jsx)(i.code,{children:"UserTopic"})," model will be automatically attached."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional tags for the device (indexed key-value data)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional metadata for the device (not indexed)."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_register-result",children:"device_register result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"The device ID that was registered/updated."})]})})]}),"\n",(0,t.jsx)(i.h3,{id:"device_update",children:"device_update"}),"\n",(0,t.jsx)(i.p,{children:"Call this method to update device. For example, when user logs out the app and you need to detach user ID from the device."}),"\n",(0,t.jsx)(i.h4,{id:"device_update-request",children:"device_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Device ids to filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Device users filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsx)(i.td,{children:"repeated DeviceProviderTokens"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens filter"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user_update"})}),(0,t.jsx)(i.td,{children:"DeviceUserUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional user update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta_update"})}),(0,t.jsx)(i.td,{children:"DeviceMetaUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional device meta update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags_update"})}),(0,t.jsx)(i.td,{children:"DeviceTagsUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional device tags update object"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics_update"})}),(0,t.jsx)(i.td,{children:"DeviceChannelsUpdate"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional topics update object"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceUserUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceMetaUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Meta to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTagsUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Tags to set"})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceChannelsUpdate"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channels to set"})]})})]}),"\n",(0,t.jsx)(i.h4,{id:"device_update-result",children:"device_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_remove",children:"device_remove"}),"\n",(0,t.jsx)(i.p,{children:"Removes device from storage. This may be also called when user logs out the app and you don't need its device token after that."}),"\n",(0,t.jsx)(i.h4,{id:"device_remove-request",children:"device_remove request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"A list of device IDs to be removed"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"A list of device user IDs to filter devices to remove"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ProviderTokens"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to remove"})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_remove-result",children:"device_remove result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_list",children:"device_list"}),"\n",(0,t.jsx)(i.p,{children:"Returns a paginated list of registered devices according to request filter conditions."}),"\n",(0,t.jsx)(i.h4,{id:"device_list-request",children:"device_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device IDs to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"providers"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device token providers to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider_tokens"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"ProviderTokens"})]}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platforms"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device platforms to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last device id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Maximum number of devices to retrieve."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_topics"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include topics information for each device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_tags"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include tags information for each device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_meta"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include meta information for each device."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_list-result",children:"device_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"Device"})]}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A list of devices"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"Device"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"provider"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's token provider."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"token"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's token."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"platform"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The device's platform."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"The user associated with the device."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"array of strings"}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_topics"})," was true"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"tags"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_tags"})," was true"]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsxs)(i.td,{children:["Only included if ",(0,t.jsx)(i.code,{children:"include_meta"})," was true"]})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"device_topic_update",children:"device_topic_update"}),"\n",(0,t.jsx)(i.p,{children:"Manage mapping of device to topics."}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_update-request",children:"device_topic_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"add"})," or ",(0,t.jsx)(i.code,{children:"remove"})," or ",(0,t.jsx)(i.code,{children:"set"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_update-result",children:"device_topic_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"device_topic_list",children:"device_topic_list"}),"\n",(0,t.jsx)(i.p,{children:"List device to topic mapping."}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_list-request",children:"device_topic_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device IDs to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_providers"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device token providers to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_provider_tokens"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"ProviderTokens"})]}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Provider tokens to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_platforms"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device platforms to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of device users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic_prefix"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Channel prefix to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last device id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Maximum number of devices to retrieve."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_device"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Flag indicating whether to include Device information for each object."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"device_topic_list-result",children:"device_topic_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"DeviceChannel"})]}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A list of DeviceChannel objects"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceChannel"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"ID of DeviceChannel"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channel"})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"user_topic_update",children:"user_topic_update"}),"\n",(0,t.jsx)(i.p,{children:"Manage mapping of topics with users. These user topics will be automatically attached to user devices upon registering. And removed from device upon deattaching user."}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_update-request",children:"user_topic_update request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User ID."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"add"})," or ",(0,t.jsx)(i.code,{children:"remove"})," or ",(0,t.jsx)(i.code,{children:"set"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics."})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_update-result",children:"user_topic_update result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h3,{id:"user_topic_list",children:"user_topic_list"}),"\n",(0,t.jsx)(i.p,{children:"List user to topic mapping."}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_list-request",children:"user_topic_list request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"users"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of users to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"List of topics to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic_prefix"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Channel prefix to filter results."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"since"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor for pagination (last id in previous batch, empty for first page)."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"limit"})}),(0,t.jsx)(i.td,{children:"int32"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Maximum number of ",(0,t.jsx)(i.code,{children:"UserTopic"})," objects to retrieve."]})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"user_topic_list-result",children:"user_topic_list result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"items"})}),(0,t.jsxs)(i.td,{children:["repeated ",(0,t.jsx)(i.code,{children:"UserTopic"})]}),(0,t.jsx)(i.td,{children:"A list of UserTopic objects"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"has_more"})}),(0,t.jsx)(i.td,{children:"bool"}),(0,t.jsx)(i.td,{children:"A flag indicating whether there are more devices available"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"UserTopic"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["ID of ",(0,t.jsx)(i.code,{children:"UserTopic"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"user"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"User ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Channel"})]})]})]}),"\n",(0,t.jsx)(i.h3,{id:"send_push_notification",children:"send_push_notification"}),"\n",(0,t.jsxs)(i.p,{children:["Send push notification to specific ",(0,t.jsx)(i.code,{children:"device_ids"}),", or to ",(0,t.jsx)(i.code,{children:"topics"}),", or native provider identifiers like ",(0,t.jsx)(i.code,{children:"fcm_tokens"}),", or to ",(0,t.jsx)(i.code,{children:"fcm_topic"}),". Request will be queued by Centrifugo, consumed by Centrifugo built-in workers and sent to the provider API."]}),"\n",(0,t.jsx)(i.h4,{id:"send_push_notification-request",children:"send_push_notification request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"recipient"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"PushRecipient"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Recipient of push notification"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"notification"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"PushNotification"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Push notification to send"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"PushRecipient"})," (you ",(0,t.jsx)(i.strong,{children:"must set only one of the following fields"}),"):"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_ids"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of device IDs (managed by Centrifugo)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"topics"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to topics (managed by Centrifugo)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of FCM native tokens"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a FCM native topic"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm_condition"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a FCM native condition"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of HMS native tokens"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_topic"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a HMS native topic"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms_condition"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a HMS native condition"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"apns_tokens"})}),(0,t.jsx)(i.td,{children:"repeated string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to a list of APNs native tokens"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"PushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Unique send id, used for Centrifugo builtin analytics"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"expire_at"})}),(0,t.jsx)(i.td,{children:"int64"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Unix timestamp when Centrifugo stops attempting to send this notification (this does not relate to notification TTL fields)"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"fcm"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"FcmPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for FCM"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"hms"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"HmsPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for HMS"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"apns"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"ApnsPushNotification"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Notification for APNs"})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"FcmPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"message"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["FCM ",(0,t.jsx)(i.a,{href:"https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Message",children:"Message"})," described in FCM docs."]})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"HmsPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"message"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["HMS ",(0,t.jsx)(i.a,{href:"https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197#EN-US_TOPIC_0000001134031085__p1324218481619",children:"Message"})," described in HMS Push Kit docs."]})]})})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"ApnsPushNotification"}),":"]}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"headers"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map<string, string>"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["APNs ",(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns",children:"headers"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"payload"})}),(0,t.jsx)(i.td,{children:"JSON object"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["APNs ",(0,t.jsx)(i.a,{href:"https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification",children:"payload"})]})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"send_push_notification-result",children:"send_push_notification result"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field Name"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsxs)(i.td,{children:["Unique send id, matches ",(0,t.jsx)(i.code,{children:"uid"})," in request if it was provided"]})]})})]}),"\n",(0,t.jsx)(i.h3,{id:"update_push_status",children:"update_push_status"}),"\n",(0,t.jsx)(i.p,{children:"This API call is experimental, some changes may happen here."}),"\n",(0,t.jsxs)(i.p,{children:["Centrifugo PRO also allows tracking status of push notification delivery and interaction. It's possible to use ",(0,t.jsx)(i.code,{children:"update_push_status"})," API to save the updated status of push notification to the ",(0,t.jsx)(i.code,{children:"notifications"})," ",(0,t.jsx)(i.a,{href:"/docs/4/pro/analytics#notifications-table",children:"analytics table"}),". Then it's possible to build insights into push notification effectiveness by querying the table."]}),"\n",(0,t.jsxs)(i.p,{children:["The ",(0,t.jsx)(i.code,{children:"update_push_status"})," API supposes that you are using ",(0,t.jsx)(i.code,{children:"uid"})," field with each notification sent and you are using Centrifugo PRO generated device IDs (as described in ",(0,t.jsx)(i.a,{href:"#steps-to-integrate",children:"steps to integrate"}),")."]}),"\n",(0,t.jsx)(i.p,{children:"This is a part of server API at the moment, so you need to proxy requests to this endpoint over your backend. We can consider making this API suitable for requests from the client side \u2013 please reach out if your use case requires it."}),"\n",(0,t.jsx)(i.h4,{id:"update_push_status-request",children:"update_push_status request"}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Field"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Required"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"uid"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"uid"})," (unique send id) from ",(0,t.jsx)(i.code,{children:"send_push_notification"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"status"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Status of push notification - ",(0,t.jsx)(i.code,{children:"delivered"})," or ",(0,t.jsx)(i.code,{children:"interacted"})]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"device_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"Device ID"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"msg_id"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Message ID"})]})]})]}),"\n",(0,t.jsx)(i.h4,{id:"update_push_status-result",children:"update_push_status result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h2,{id:"metrics",children:"Metrics"}),"\n",(0,t.jsx)(i.p,{children:"Several metrics are available to monitor the state of Centrifugo push worker system:"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_notification_count"})," - counter, shows total count of push notifications sent to providers (splitted by provider, recipient type, platform, success, error code)."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_queue_consuming_lag"})," - gauge, shows the lag of queues, should be close to zero most of the time. Splitted by provider and name of queue."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_consuming_inflight_jobs"})," - gauge, shows immediate number of workers proceccing pushes. Splitted by provider and name of queue."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"centrifugo_push_job_duration_seconds"})," - summary, provides insights about worker job duration timings. Splitted by provider and recipient type."]}),"\n"]}),"\n",(0,t.jsx)(i.h2,{id:"further-reading-and-tutorials",children:"Further reading and tutorials"}),"\n",(0,t.jsx)(i.p,{children:"Coming soon."})]})}function a(e={}){const{wrapper:i}={...(0,d.a)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},88404:(e,i,s)=>{s.d(i,{Z:()=>t});const t=s.p+"assets/images/push_notifications-c1af39fb6bbb1da727bd940368acd4f8.png"},11151:(e,i,s)=>{s.d(i,{Z:()=>c,a:()=>r});var t=s(67294);const d={},n=t.createContext(d);function r(e){const i=t.useContext(n);return t.useMemo((function(){return"function"==typeof e?e(i):{...i,...e}}),[i,e])}function c(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:r(e.components),t.createElement(n.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4268d52f.ee202a05.js b/assets/js/4268d52f.929a6832.js similarity index 99% rename from assets/js/4268d52f.ee202a05.js rename to assets/js/4268d52f.929a6832.js index ace4478bb..e4cab521a 100644 --- a/assets/js/4268d52f.ee202a05.js +++ b/assets/js/4268d52f.929a6832.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[895],{84096:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>c,toc:()=>l});var r=s(85893),i=s(11151);const t={id:"proxy",title:"Proxy events to the backend"},o=void 0,c={id:"server/proxy",title:"Proxy events to the backend",description:"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.",source:"@site/versioned_docs/version-4/server/proxy.md",sourceDirName:"server",slug:"/server/proxy",permalink:"/docs/4/server/proxy",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/server/proxy.md",tags:[],version:"4",frontMatter:{id:"proxy",title:"Proxy events to the backend"},sidebar:"Guides",previous:{title:"Engines and scalability",permalink:"/docs/4/server/engines"},next:{title:"History and recovery",permalink:"/docs/4/server/history_and_recovery"}},d={},l=[{value:"HTTP proxy",id:"http-proxy",level:2},{value:"HTTP request structure",id:"http-request-structure",level:3},{value:"Proxy HTTP headers",id:"proxy-http-headers",level:3},{value:"Proxy GRPC metadata",id:"proxy-grpc-metadata",level:3},{value:"Connect proxy",id:"connect-proxy",level:3},{value:"Connect request fields",id:"connect-request-fields",level:4},{value:"Connect result fields",id:"connect-result-fields",level:4},{value:"Options",id:"options",level:4},{value:"Example",id:"example",level:4},{value:"What if connection is unauthenticated/unauthorized to connect?",id:"what-if-connection-is-unauthenticatedunauthorized-to-connect",level:4},{value:"Refresh proxy",id:"refresh-proxy",level:3},{value:"Refresh request fields",id:"refresh-request-fields",level:4},{value:"Refresh result fields",id:"refresh-result-fields",level:4},{value:"Options",id:"options-1",level:4},{value:"RPC proxy",id:"rpc-proxy",level:3},{value:"RPC request fields",id:"rpc-request-fields",level:4},{value:"RPC result fields",id:"rpc-result-fields",level:4},{value:"Options",id:"options-2",level:4},{value:"Subscribe proxy",id:"subscribe-proxy",level:3},{value:"Subscribe request fields",id:"subscribe-request-fields",level:4},{value:"Subscribe result fields",id:"subscribe-result-fields",level:4},{value:"Override object",id:"override-object",level:4},{value:"Options",id:"options-3",level:4},{value:"What if connection is not allowed to subscribe?",id:"what-if-connection-is-not-allowed-to-subscribe",level:4},{value:"Publish proxy",id:"publish-proxy",level:3},{value:"Publish request fields",id:"publish-request-fields",level:4},{value:"Publish result fields",id:"publish-result-fields",level:4},{value:"Options",id:"options-4",level:4},{value:"Sub refresh proxy",id:"sub-refresh-proxy",level:3},{value:"Sub refresh request fields",id:"sub-refresh-request-fields",level:4},{value:"Sub refresh result fields",id:"sub-refresh-result-fields",level:4},{value:"Options",id:"options-5",level:4},{value:"Return custom error",id:"return-custom-error",level:3},{value:"Return custom disconnect",id:"return-custom-disconnect",level:3},{value:"GRPC proxy",id:"grpc-proxy",level:2},{value:"GRPC proxy options",id:"grpc-proxy-options",level:3},{value:"proxy_grpc_cert_file",id:"proxy_grpc_cert_file",level:4},{value:"proxy_grpc_credentials_key",id:"proxy_grpc_credentials_key",level:4},{value:"proxy_grpc_credentials_value",id:"proxy_grpc_credentials_value",level:4},{value:"GRPC proxy example",id:"grpc-proxy-example",level:3},{value:"Header proxy rules",id:"header-proxy-rules",level:2},{value:"Binary mode",id:"binary-mode",level:2},{value:"Granular proxy mode",id:"granular-proxy-mode",level:2},{value:"Enable granular proxy mode",id:"enable-granular-proxy-mode",level:3},{value:"Defining a list of proxies",id:"defining-a-list-of-proxies",level:3},{value:"Granular connect and refresh",id:"granular-connect-and-refresh",level:3},{value:"Granular subscribe, publish, sub refresh",id:"granular-subscribe-publish-sub-refresh",level:3},{value:"Granular RPC",id:"granular-rpc",level:3}];function a(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.p,{children:"The list of events that can be proxied:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"connect"})," \u2013 called when a client connects to Centrifugo, so it's possible to authenticate user, return custom data to a client, subscribe connection to several channels, attach meta information to the connection, and so on. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"refresh"})," - called when a client session is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical connection liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"subscribe"})," - called when clients try to subscribe on a channel, so it's possible to check permissions and return custom initial subscription data. Works for bidirectional transports only."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"publish"})," - called when a client tries to publish into a channel, so it's possible to check permissions and optionally modify publication data. Works for bidirectional transports only."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"sub_refresh"})," - called when a client subscription is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical subscription liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"rpc"})," - called when a client sends RPC, you can do whatever logic you need based on a client-provided RPC method and params. Works for bidirectional transports only."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"At the moment Centrifugo can proxy these events over two protocols:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"HTTP (JSON payloads)"}),"\n",(0,r.jsx)(n.li,{children:"GRPC (Protobuf messages)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"http-proxy",children:"HTTP proxy"}),"\n",(0,r.jsx)(n.p,{children:"HTTP proxy in Centrifugo converts client connection events into HTTP calls to the application backend."}),"\n",(0,r.jsx)(n.h3,{id:"http-request-structure",children:"HTTP request structure"}),"\n",(0,r.jsxs)(n.p,{children:["All proxy calls are ",(0,r.jsx)(n.strong,{children:"HTTP POST"})," requests that will be sent from Centrifugo to configured endpoints with a configured timeout. These requests will have some headers copied from the original client request (see details below) and include JSON body which varies depending on call type (for example data sent by a client in RPC call etc, see more details about JSON bodies below)."]}),"\n",(0,r.jsx)(n.h3,{id:"proxy-http-headers",children:"Proxy HTTP headers"}),"\n",(0,r.jsx)(n.p,{children:"The good thing about Centrifugo HTTP proxy is that it transparently proxies original HTTP request headers in a request to app backend. In most cases this allows achieving transparent authentication on the application backend side. But it's required to provide an explicit list of HTTP headers you want to be proxied, for example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Alternatively, you can set a list of headers via an environment variable (space separated):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'export CENTRIFUGO_PROXY_HTTP_HEADERS="Cookie User-Agent X-B3-TraceId X-B3-SpanId" ./centrifugo\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Centrifugo forces the",(0,r.jsx)(n.code,{children:" Content-Type"})," header to be ",(0,r.jsx)(n.code,{children:"application/json"})," in all HTTP proxy requests since it sends the body in JSON format to the application backend."]})}),"\n",(0,r.jsx)(n.h3,{id:"proxy-grpc-metadata",children:"Proxy GRPC metadata"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.a,{href:"/docs/4/transports/uni_grpc",children:"GRPC unidirectional stream"})," is used as a client transport then you may want to proxy GRPC metadata from the client request. In this case you may configure ",(0,r.jsx)(n.code,{children:"proxy_grpc_metadata"})," option. This is an array of string metadata keys which will be proxied. These metadata keys transformed to HTTP headers of proxy request. By default no metadata keys are proxied."]}),"\n",(0,r.jsxs)(n.p,{children:["See below ",(0,r.jsx)(n.a,{href:"#header-proxy-rules",children:"the table of rules"})," how metadata and headers proxied in transport/proxy different scenarios."]}),"\n",(0,r.jsx)(n.h3,{id:"connect-proxy",children:"Connect proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_connect_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 connection requests ",(0,r.jsx)(n.strong,{children:"without JWT set"})," will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," URL endpoint. On your backend side, you can authenticate the incoming connection and return client credentials to Centrifugo in response to the proxied request."]}),"\n",(0,r.jsx)(n.admonition,{type:"danger",children:(0,r.jsxs)(n.p,{children:["Make sure you properly configured ",(0,r.jsx)(n.a,{href:"/docs/4/server/configuration#allowed_origins",children:"allowed_origins"})," Centrifugo option or check request origin on your backend side upon receiving connect request from Centrifugo. Otherwise, your site can be vulnerable to CSRF attacks if you are using WebSocket transport for client connections."]})}),"\n",(0,r.jsxs)(n.p,{children:["Yes, this means you don't need to generate JWT and pass it to a client-side and can rely on a cookie while authenticating the user. ",(0,r.jsx)(n.strong,{children:"Centrifugo should work on the same domain in this case so your site cookie could be passed to Centrifugo by browsers"}),". In many cases your existing session mechanism will provide user authentication details to the connect proxy handler."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you want to pass some custom authentication token from a client side (not in Centrifugo JWT format) but force request to be proxied then you may put it in a cookie or use connection request custom ",(0,r.jsx)(n.code,{children:"data"})," field (available in all our transports). This ",(0,r.jsx)(n.code,{children:"data"})," can contain arbitrary payload you want to pass from a client to a server."]})}),"\n",(0,r.jsxs)(n.p,{children:["This also means that ",(0,r.jsx)(n.strong,{children:"every"})," new connection from a user will result in an HTTP POST request to your application backend. While with JWT token you usually generate it once on application page reload, if client reconnects due to Centrifugo restart or internet connection loss it uses the same JWT it had before thus usually no additional requests are generated during reconnect process (until JWT expired)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(40330).Z+"",width:"2600",height:"1032"})}),"\n",(0,r.jsxs)(n.p,{children:["Payload example that will be sent to app backend when client without token wants to establish a connection with Centrifugo and ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," is set to non-empty URL string:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"user": "56"}}\n'})}),"\n",(0,r.jsx)(n.p,{children:"This response allows connecting and tells Centrifugo the ID of a user. See below the full list of supported fields in the result."}),"\n",(0,r.jsx)(n.p,{children:"Several app examples which use connect proxy can be found in our blog:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"With NodeJS"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/11/04/integrating-with-django-building-chat-application",children:"With Django"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",children:"With Laravel"})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"connect-request-fields",children:"Connect request fields"}),"\n",(0,r.jsx)(n.p,{children:"This is what sent from Centrifugo to application backend in case of connect proxy request."}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional name of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"version"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional version of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from the client in base64 format (if the binary proxy mode is used)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"Array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"list of server-side channels client want to subscribe to, the application server must check permissions and add allowed channels to result"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"connect-result-fields",children:"Connect result fields"}),"\n",(0,r.jsxs)(n.p,{children:["This is what application returns to Centrifugo inside ",(0,r.jsx)(n.code,{children:"result"})," field in case of connect proxy request."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"user ID (calculated on app backend based on request cookie header for example). Return it as an empty string for accepting unauthenticated requests"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a timestamp when connection must be considered expired. If not set or set to ",(0,r.jsx)(n.code,{children:"0"})," connection won't expire at all"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in connect command response."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in the connect command response for binary connections, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["allows providing a list of server-side channels to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"subs"}),(0,r.jsx)(n.td,{children:"map of SubscribeOptions"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["map of channels with options to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsxs)(n.td,{children:["JSON object (ex. ",(0,r.jsx)(n.code,{children:'{"key": "value"}'}),")"]}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a custom data to attach to connection (this ",(0,r.jsx)(n.strong,{children:"won't be exposed to client-side"}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_connect_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h4,{id:"example",children:"Example"}),"\n",(0,r.jsxs)(n.p,{children:["Here is the simplest example of the connect handler in Tornado Python framework (note that in a real system you need to authenticate the user on your backend side, here we just return ",(0,r.jsx)(n.code,{children:'"56"'})," as user ID):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"class CentrifugoConnectHandler(tornado.web.RequestHandler):\n\n def check_xsrf_cookie(self):\n pass\n\n def post(self):\n self.set_header('Content-Type', 'application/json; charset=\"utf-8\"')\n data = json.dumps({\n 'result': {\n 'user': '56'\n }\n })\n self.write(data)\n\n\ndef main():\n options.parse_command_line()\n app = tornado.web.Application([\n (r'/centrifugo/connect', CentrifugoConnectHandler),\n ])\n app.listen(3000)\n tornado.ioloop.IOLoop.instance().start()\n\n\nif __name__ == '__main__':\n main()\n"})}),"\n",(0,r.jsx)(n.p,{children:"This example should help you to implement a similar HTTP handler in any language/framework you are using on the backend side."}),"\n",(0,r.jsxs)(n.p,{children:["We also have a tutorial in the blog about ",(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"Centrifugo integration with NodeJS"})," which uses connect proxy and native session middleware of Express.js to authenticate connections. Even if you are not using NodeJS on a backend a tutorial can help you understand the idea."]}),"\n",(0,r.jsx)(n.h4,{id:"what-if-connection-is-unauthenticatedunauthorized-to-connect",children:"What if connection is unauthenticated/unauthorized to connect?"}),"\n",(0,r.jsxs)(n.p,{children:["In this case return a disconnect object as a response. See ",(0,r.jsx)(n.a,{href:"#return-custom-disconnect",children:"Return custom disconnect"})," section. Depending on whether you want connection to reconnect or not (usually not) you can select the appropriate disconnect code. Sth like this in response:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4501,\n "reason": "unauthorized"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 may be sufficient enough. Choosing codes and reason is up to the developer, but follow the rules described in ",(0,r.jsx)(n.a,{href:"#return-custom-disconnect",children:"Return custom disconnect"})," section."]}),"\n",(0,r.jsx)(n.h3,{id:"refresh-proxy",children:"Refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_refresh_endpoint": "http://localhost:3000/centrifugo/refresh",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_refresh_endpoint"})," when it's time to refresh the connection. Centrifugo itself will ask your backend about connection validity instead of refresh workflow on the client-side."]}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in refresh request (when the connection is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"refresh-request-fields",children:"Refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"refresh-result-fields",children:"Refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-1",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_refresh_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rpc-proxy",children:"RPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_rpc_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["RPC calls over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_rpc_endpoint"}),". This allows a developer to utilize WebSocket (or SockJS) connection in a bidirectional way."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in RPC request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "method": "getCurrentPrice",\n "data":{"params": {"object_id": 12}}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"data": {"answer": "2019"}}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"rpc-request-fields",children:"RPC request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"method"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"an RPC method string, if the client does not use named RPC call then method will be omitted"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC custom data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"rpc-result-fields",children:"RPC result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC response - any valid JSON is supported"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["can be set instead of ",(0,r.jsx)(n.code,{children:"data"})," for binary response encoded in base64 format"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-2",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_rpc_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return a custom error."}),"\n",(0,r.jsx)(n.h3,{id:"subscribe-proxy",children:"Subscribe proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 subscribe requests sent over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_subscribe_endpoint"}),". This allows you to check the access of the client to a channel."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Subscribe proxy does not proxy ",(0,r.jsx)(n.a,{href:"/docs/4/server/channels#private-channel-prefix",children:"private"})," and ",(0,r.jsx)(n.a,{href:"/docs/4/server/channels#user-channel-boundary",children:"user-limited"})," channels at the moment"]}),". That's because those are already providing a level of security (user-limited channels check current user ID, private channels require subscription token). In some cases you may use subscribe proxy as a replacement for private channels actually: if you prefer to check permissions using the proxy to backend mechanism \u2013 just stop using ",(0,r.jsx)(n.code,{children:"$"})," prefixes in channels, properly configure subscribe proxy and validate subscriptions upon proxy from Centrifugo to your backend (issued each time user tries to subscribe on a channel for which subscribe proxy enabled)."]})}),"\n",(0,r.jsxs)(n.p,{children:["Unlike proxy types described above subscribe proxy must be enabled per channel namespace. This means that every namespace (including global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," that enables subscribe proxy for channels in a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable subscribe proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "proxy_subscribe": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in subscribe request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if subscription is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-request-fields",children:"Subscribe request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to subscribe to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"custom data from client sent with subscription request (this field will only be set if provided by a client on subscribe)."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional subscription data from the client in base64 format (if the binary proxy mode is used)."})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-result-fields",children:"Subscribe result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary connection channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"override"}),(0,r.jsx)(n.td,{children:"Override object"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"presence"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override presence"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_push_join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_push_join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_positioning"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_positioning"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_recovery"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_recovery"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow subscribing."}),"\n",(0,r.jsx)(n.h4,{id:"options-3",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_subscribe_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h4,{id:"what-if-connection-is-not-allowed-to-subscribe",children:"What if connection is not allowed to subscribe?"}),"\n",(0,r.jsxs)(n.p,{children:["In this case you can return error object as a subscribe handler response. See ",(0,r.jsx)(n.a,{href:"#return-custom-error",children:"return custom error"})," section."]}),"\n",(0,r.jsx)(n.p,{children:"In general, frontend applications should not try to subscribe to channels for which access is not allowed. But these situations can happen or malicious user can try to subscribe to a channel. In most scenarios returning:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 403,\n "message": "permission denied"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 is sufficient enough. Error code may be not 403 actually, no real reason to force HTTP semantics here - so it's up to Centrifugo user to decide. Just keep it in range [400, 1999] as described ",(0,r.jsx)(n.a,{href:"#return-custom-error",children:"here"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["If case of returning response above, on client side ",(0,r.jsx)(n.code,{children:"unsubscribed"})," event of Subscription object will be called with error code 403. Subscription won't resubscribe automatically after that."]}),"\n",(0,r.jsx)(n.h3,{id:"publish-proxy",children:"Publish proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 publish calls sent by a client will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_publish_endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"This request happens BEFORE a message is published to a channel, so your backend can validate whether a client can publish data to a channel. An important thing here is that publication to the channel can fail after your backend successfully validated publish request (for example publish to Redis by Centrifugo returned an error). In this case, your backend won't know about the error that happened but this error will propagate to the client-side."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(28719).Z+"",width:"2600",height:"1098"})}),"\n",(0,r.jsxs)(n.p,{children:["Like the subscribe proxy, publish proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_publish"})," that enables publish proxy for channels in the namespace. All other namespace options will be taken into account before making a proxy request, so you also need to turn on the ",(0,r.jsx)(n.code,{children:"publish"})," option too."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable publish proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_publish"})," and ",(0,r.jsx)(n.code,{children:"publish"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "publish": true,\n "proxy_publish": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "publish": true,\n "proxy_publish": true\n }]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Keep in mind that this will only work if the ",(0,r.jsx)(n.code,{children:"publish"})," channel option is on for a channel namespace (or for a global top-level namespace)."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in a publish request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index",\n "data":{"input":"hello"}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if publish is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"publish-request-fields",children:"Publish request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to publish to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"publish-result-fields",children:"Publish result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["an optional JSON data to send into a channel ",(0,r.jsx)(n.strong,{children:"instead of"})," original data sent by a client"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary data encoded in base64 format, the meaning is the same as for data above, will be decoded to raw bytes on Centrifugo side before publishing"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"skip_history"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["when set to ",(0,r.jsx)(n.code,{children:"true"})," Centrifugo won't save publication to the channel history"]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow publishing."}),"\n",(0,r.jsx)(n.h4,{id:"options-4",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_publish_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"sub-refresh-proxy",children:"Sub refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"Added in Centrifugo v4.1.1"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/sub_refresh",\n "proxy_sub_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh_endpoint"})," when it's time to refresh the subscription. Centrifugo itself will ask your backend about subscription validity instead of subscription refresh workflow on the client-side."]}),"\n",(0,r.jsxs)(n.p,{children:["Like subscribe and publish proxy types, sub refresh proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh"})," that enables sub refresh proxy for channels in the namespace. Only subscriptions which have expiration time will be validated over sub refresh proxy endpoint."]}),"\n",(0,r.jsx)(n.p,{children:"Sub refresh proxy may be used as a periodical Subscription liveness callback from Centrifugo to app backend."}),"\n",(0,r.jsx)(n.admonition,{type:"caution",children:(0,r.jsx)(n.p,{children:"In the current implementation the delay of Subscription refresh requests from Centrifugo to application backend may be up to one minute (was implemented this way from a simplicity and efficiency perspective). We assume this should be enough for many scenarios. But this may be improved if needed. Please reach us out with a detailed description of your use case where you want more accurate requests to refresh subscriptions."})}),"\n",(0,r.jsxs)(n.p,{children:["So to enable sub refresh proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/sub_refresh",\n "proxy_sub_refresh": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/publish",\n "namespaces": [{\n "name": "sun",\n "proxy_sub_refresh": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in sub refresh request (when the subscription is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "channel"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"sub-refresh-request-fields",children:"Sub refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"channel for which Subscription is going to expire"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"sub-refresh-result-fields",children:"Sub refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-5",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_sub_refresh_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-error",children:"Return custom error"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains an error to return it to the client:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 1000,\n "message": "custom error"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Applications ",(0,r.jsx)(n.strong,{children:"must use error codes in range [400, 1999]"}),". Error code field is ",(0,r.jsx)(n.code,{children:"uint32"})," internally."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsx)(n.p,{children:"Returning custom error does not apply to response for refresh and sub refresh proxy requests as there is no sense in returning an error (will not reach client anyway)."})}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-disconnect",children:"Return custom disconnect"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains a custom disconnect object to disconnect client in a custom way:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4500,\n "reason": "disconnect reason"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"must use numbers in the range 4000-4999 for custom disconnect codes"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"codes in range [4000, 4499] give client an advice to reconnect"}),"\n",(0,r.jsx)(n.li,{children:"codes in range [4500, 4999] are terminal codes \u2013 client won't reconnect upon receiving it."}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Code is ",(0,r.jsx)(n.code,{children:"uint32"})," internally. Numbers outside of 4000-4999 range are reserved by Centrifugo internal protocol. Keep in mind that ",(0,r.jsx)(n.strong,{children:"due to WebSocket protocol limitations and Centrifugo internal protocol needs you need to keep disconnect reason string no longer than 32 ASCII symbols (i.e. 32 bytes max)"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Returning custom disconnect does not apply to response for refresh and sub refresh proxy requests as there is no way to control disconnect at moment - the client will always be disconnected with ",(0,r.jsx)(n.code,{children:"expired"})," disconnect reason."]})}),"\n",(0,r.jsx)(n.h2,{id:"grpc-proxy",children:"GRPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can also proxy connection events to your backend over GRPC instead of HTTP. In this case, Centrifugo acts as a GRPC client and your backend acts as a GRPC server."}),"\n",(0,r.jsxs)(n.p,{children:["GRPC service definitions can be found in the Centrifugo repository: ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/blob/master/internal/proxyproto/proxy.proto",children:"proxy.proto"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"GRPC proxy inherits all the fields for HTTP proxy \u2013 so you can refer to field descriptions for HTTP above. Both proxy types in Centrifugo share the same Protobuf schema definitions."})}),"\n",(0,r.jsx)(n.p,{children:"Every proxy call in this case is a unary GRPC call. Centrifugo puts client headers into GRPC metadata (since GRPC doesn't have headers concept)."}),"\n",(0,r.jsxs)(n.p,{children:["All you need to do to enable proxying over GRPC instead of HTTP is to use ",(0,r.jsx)(n.code,{children:"grpc"})," schema in endpoint, for example for the connect proxy:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_connect_endpoint": "grpc://localhost:12000",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_refresh_endpoint": "grpc://localhost:12000",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or for RPC proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_rpc_endpoint": "grpc://localhost:12000",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["For publish proxy in namespace ",(0,r.jsx)(n.code,{children:"chat"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_publish_endpoint": "grpc://localhost:12000",\n "proxy_publish_timeout": "1s"\n "namespaces": [\n {\n "name": "chat",\n "publish": true,\n "proxy_publish": true\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Use subscribe proxy for all channels without namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_subscribe_endpoint": "grpc://localhost:12000",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"So the same as for HTTP, just the different endpoint scheme."}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-options",children:"GRPC proxy options"}),"\n",(0,r.jsx)(n.p,{children:"Some additional options exist to control GRPC proxy behavior."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_cert_file",children:"proxy_grpc_cert_file"}),"\n",(0,r.jsxs)(n.p,{children:["String, default: ",(0,r.jsx)(n.code,{children:'""'}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_key",children:"proxy_grpc_credentials_key"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsx)(n.p,{children:"Add custom key to per-RPC credentials."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_value",children:"proxy_grpc_credentials_value"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsxs)(n.p,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"proxy_grpc_credentials_key"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-example",children:"GRPC proxy example"}),"\n",(0,r.jsxs)(n.p,{children:["We have ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/go_proxy/grpc",children:"an example of backend server"})," (written in Go language) which can react to events from Centrifugo over GRPC. For other programming languages the approach is similar, i.e.:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Copy proxy Protobuf definitions"}),"\n",(0,r.jsx)(n.li,{children:"Generate GRPC code"}),"\n",(0,r.jsx)(n.li,{children:"Run backend service with you custom business logic"}),"\n",(0,r.jsx)(n.li,{children:"Point Centrifugo to it."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"header-proxy-rules",children:"Header proxy rules"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo not only supports HTTP-based client transports but also GRPC-based (for example GRPC unidirectional stream). Here is a table with rules used to proxy headers/metadata in various scenarios:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Client protocol type"}),(0,r.jsx)(n.th,{children:"Proxy type"}),(0,r.jsx)(n.th,{children:"Client headers"}),(0,r.jsx)(n.th,{children:"Client metadata"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"In proxy request headers"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request headers"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"binary-mode",children:"Binary mode"}),"\n",(0,r.jsxs)(n.p,{children:["As you may noticed there are several fields in request/result description of various proxy calls which use ",(0,r.jsx)(n.code,{children:"base64"})," encoding."]}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can work with binary Protobuf protocol (in case of bidirectional WebSocket transport). All our bidirectional clients support this."}),"\n",(0,r.jsx)(n.p,{children:"Most Centrifugo users use JSON for custom payloads: i.e. for data sent to a channel, for connection info attached while authenticating (which becomes part of presence response, join/leave messages and added to Publication client info when message published from a client side)."}),"\n",(0,r.jsx)(n.p,{children:"But since HTTP proxy works with JSON format (i.e. sends requests with JSON body) \u2013 it can not properly pass binary data to application backend. Arbitrary binary data can't be encoded into JSON."}),"\n",(0,r.jsx)(n.p,{children:"In this case it's possible to turn Centrifugo proxy into binary mode by using:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_binary_encoding": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Once enabled this option tells Centrifugo to use base64 format in requests and utilize fields like ",(0,r.jsx)(n.code,{children:"b64data"}),", ",(0,r.jsx)(n.code,{children:"b64info"})," with payloads encoded to base64 instead of their JSON field analogues."]}),"\n",(0,r.jsx)(n.p,{children:"While this feature is useful for HTTP proxy it's not really required if you are using GRPC proxy \u2013 since GRPC allows passing binary data just fine."}),"\n",(0,r.jsx)(n.p,{children:"Regarding b64 fields in proxy results \u2013 just use base64 fields when required \u2013 Centrifugo is smart enough to detect that you are using base64 field and will pick payload from it, decode from base64 automatically and will pass further to connections in binary format."}),"\n",(0,r.jsx)(n.h2,{id:"granular-proxy-mode",children:"Granular proxy mode"}),"\n",(0,r.jsx)(n.p,{children:"By default, with proxy configuration shown above, you can only define a global proxy settings and one endpoint for each type of proxy (i.e. one for connect proxy, one for subscribe proxy, and so on). Also, you can configure only one set of headers to proxy which will be used by each proxy type. This may be sufficient for many use cases, but what if you need a more granular control? For example, use different subscribe proxy endpoints for different channel namespaces (i.e. when using microservice architecture)."}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo v3.1.0 introduced a new mode for proxy configuration called granular proxy mode. In this mode it's possible to configure subscribe and publish proxy behaviour on per-namespace level, use different set of headers passed to the proxy endpoint in each proxy type. Also, Centrifugo v3.1.0 introduced a concept of rpc namespaces (in addition to channel namespaces) \u2013 together with granular proxy mode this allows configuring rpc proxies on per rpc namespace basis."}),"\n",(0,r.jsx)(n.h3,{id:"enable-granular-proxy-mode",children:"Enable granular proxy mode"}),"\n",(0,r.jsxs)(n.p,{children:["Since the change is rather radical it requires a separate boolean option ",(0,r.jsx)(n.code,{children:"granular_proxy_mode"})," to be enabled. As soon as this option set Centrifugo does not use proxy configuration rules described above and follows the rules described below."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"defining-a-list-of-proxies",children:"Defining a list of proxies"}),"\n",(0,r.jsxs)(n.p,{children:["When using granular proxy mode on configuration top level you can define ",(0,r.jsx)(n.code,{children:'"proxies"'})," array with a list of different proxy objects. Each proxy object in an array should have at least two required fields: ",(0,r.jsx)(n.code,{children:"name"})," and ",(0,r.jsx)(n.code,{children:"endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Here is an example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [\n {\n "name": "connect",\n "endpoint": "http://localhost:3000/centrifugo/connect",\n "timeout": "500ms",\n "http_headers": ["Cookie"]\n },\n {\n "name": "refresh",\n "endpoint": "http://localhost:3000/centrifugo/refresh",\n "timeout": "500ms"\n },\n {\n "name": "subscribe1",\n "endpoint": "http://localhost:3001/centrifugo/subscribe"\n },\n {\n "name": "publish1",\n "endpoint": "http://localhost:3001/centrifugo/publish"\n },\n {\n "name": "rpc1",\n "endpoint": "http://localhost:3001/centrifugo/rpc"\n },\n {\n "name": "subscribe2",\n "endpoint": "http://localhost:3002/centrifugo/subscribe"\n },\n {\n "name": "publish2",\n "endpoint": "grpc://localhost:3002"\n }\n {\n "name": "rpc2",\n "endpoint": "grpc://localhost:3002"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Let's look at all fields for a proxy object which is possible to set for each proxy inside ",(0,r.jsx)(n.code,{children:'"proxies"'})," array."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field name"}),(0,r.jsx)(n.th,{children:"Field type"}),(0,r.jsx)(n.th,{children:"Required"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["Unique name of proxy used for referencing in configuration, must match regexp ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"endpoint"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["HTTP or GRPC endpoint in the same format as in default proxy mode. For example, ",(0,r.jsx)(n.code,{children:"http://localhost:3000/path"})," for HTTP or ",(0,r.jsx)(n.code,{children:"grpc://localhost:3000"})," for GRPC."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"timeout"}),(0,r.jsx)(n.td,{children:"duration (string)"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["Proxy request timeout, default ",(0,r.jsx)(n.code,{children:'"1s"'})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"http_headers"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of headers to proxy, by default no headers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_metadata"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of GRPC metadata keys to proxy, by default no metadata keys"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"binary_encoding"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Use base64 for payloads"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"include_connection_meta"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Include meta information (attached on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_cert_file"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_key"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Add custom key to per-RPC credentials."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_value"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"grpc_credentials_key"}),"."]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"granular-connect-and-refresh",children:"Granular connect and refresh"}),"\n",(0,r.jsx)(n.p,{children:"As soon as you defined a list of proxies you can reference them by a name to use a specific proxy configuration for a specific event."}),"\n",(0,r.jsx)(n.p,{children:"To enable connect proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["We have an ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/nodejs_granular_proxy",children:"example of Centrifugo integration with NodeJS"})," which uses granular proxy mode. Even if you are not using NodeJS on a backend an example can help you understand the idea."]}),"\n",(0,r.jsx)(n.p,{children:"Let's also add refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect",\n "refresh_proxy_name": "refresh"\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"granular-subscribe-publish-sub-refresh",children:"Granular subscribe, publish, sub refresh"}),"\n",(0,r.jsxs)(n.p,{children:["Subscribe, publish and sub refresh proxies work per-namespace. This means that ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"sub_refresh_proxy_name"})," are just channel namespace options. So it's possible to define these options on configuration top-level (for channels in default top-level namespace) or inside namespace object."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [\n {\n "name": "ns1",\n "subscribe_proxy_name": "subscribe1",\n "publish": true,\n "publish_proxy_name": "publish1"\n },\n {\n "name": "ns2",\n "subscribe_proxy_name": "subscribe2",\n "publish": true,\n "publish_proxy_name": "publish2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," is empty then no subscribe proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," is empty then no publish proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"sub_refresh_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"sub_refresh_proxy_name"'})," is empty then no sub refresh proxy will be used for a namespace."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["You can define ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"publish_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"sub_refresh_proxy_name"})," on configuration top level \u2013 and in this case publish, subscribe and sub refresh requests for channels without explicit namespace will be proxied using this proxy. The same mechanics as for other channel options in Centrifugo."]})}),"\n",(0,r.jsx)(n.h3,{id:"granular-rpc",children:"Granular RPC"}),"\n",(0,r.jsx)(n.p,{children:"Analogous to channel namespaces it's possible to configure rpc namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [...],\n "rpc_namespaces": [\n {\n "name": "rpc_ns1",\n "rpc_proxy_name": "rpc1",\n },\n {\n "name": "rpc_ns2",\n "rpc_proxy_name": "rpc2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The mechanics is the same as for channel namespaces. RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns1:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc1"}),", RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns2:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc2"}),". So Centrifugo uses ",(0,r.jsx)(n.code,{children:":"})," as RPC namespace boundary in RPC method (just like it does for channel namespaces)."]}),"\n",(0,r.jsxs)(n.p,{children:["Just like channel namespaces RPC namespaces should have a name which match ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})," regexp pattern \u2013 this is validated on Centrifugo start."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["The same as for channel namespaces and channel options you can define ",(0,r.jsx)(n.code,{children:"rpc_proxy_name"})," on configuration top level \u2013 and in this case RPC calls without explicit namespace in RPC method will be proxied using this proxy."]})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},40330:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_connect_proxy-4318d8beb2c7553d9b30b2ed7fb8edac.png"},28719:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_publish_proxy-66ccb1e8b37ed8912d218b4529597bd9.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>c,a:()=>o});var r=s(67294);const i={},t=r.createContext(i);function o(e){const n=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[895],{84096:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>c,toc:()=>l});var r=s(85893),i=s(11151);const t={id:"proxy",title:"Proxy events to the backend"},o=void 0,c={id:"server/proxy",title:"Proxy events to the backend",description:"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.",source:"@site/versioned_docs/version-4/server/proxy.md",sourceDirName:"server",slug:"/server/proxy",permalink:"/docs/4/server/proxy",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/server/proxy.md",tags:[],version:"4",frontMatter:{id:"proxy",title:"Proxy events to the backend"},sidebar:"Guides",previous:{title:"Engines and scalability",permalink:"/docs/4/server/engines"},next:{title:"History and recovery",permalink:"/docs/4/server/history_and_recovery"}},d={},l=[{value:"HTTP proxy",id:"http-proxy",level:2},{value:"HTTP request structure",id:"http-request-structure",level:3},{value:"Proxy HTTP headers",id:"proxy-http-headers",level:3},{value:"Proxy GRPC metadata",id:"proxy-grpc-metadata",level:3},{value:"Connect proxy",id:"connect-proxy",level:3},{value:"Connect request fields",id:"connect-request-fields",level:4},{value:"Connect result fields",id:"connect-result-fields",level:4},{value:"Options",id:"options",level:4},{value:"Example",id:"example",level:4},{value:"What if connection is unauthenticated/unauthorized to connect?",id:"what-if-connection-is-unauthenticatedunauthorized-to-connect",level:4},{value:"Refresh proxy",id:"refresh-proxy",level:3},{value:"Refresh request fields",id:"refresh-request-fields",level:4},{value:"Refresh result fields",id:"refresh-result-fields",level:4},{value:"Options",id:"options-1",level:4},{value:"RPC proxy",id:"rpc-proxy",level:3},{value:"RPC request fields",id:"rpc-request-fields",level:4},{value:"RPC result fields",id:"rpc-result-fields",level:4},{value:"Options",id:"options-2",level:4},{value:"Subscribe proxy",id:"subscribe-proxy",level:3},{value:"Subscribe request fields",id:"subscribe-request-fields",level:4},{value:"Subscribe result fields",id:"subscribe-result-fields",level:4},{value:"Override object",id:"override-object",level:4},{value:"Options",id:"options-3",level:4},{value:"What if connection is not allowed to subscribe?",id:"what-if-connection-is-not-allowed-to-subscribe",level:4},{value:"Publish proxy",id:"publish-proxy",level:3},{value:"Publish request fields",id:"publish-request-fields",level:4},{value:"Publish result fields",id:"publish-result-fields",level:4},{value:"Options",id:"options-4",level:4},{value:"Sub refresh proxy",id:"sub-refresh-proxy",level:3},{value:"Sub refresh request fields",id:"sub-refresh-request-fields",level:4},{value:"Sub refresh result fields",id:"sub-refresh-result-fields",level:4},{value:"Options",id:"options-5",level:4},{value:"Return custom error",id:"return-custom-error",level:3},{value:"Return custom disconnect",id:"return-custom-disconnect",level:3},{value:"GRPC proxy",id:"grpc-proxy",level:2},{value:"GRPC proxy options",id:"grpc-proxy-options",level:3},{value:"proxy_grpc_cert_file",id:"proxy_grpc_cert_file",level:4},{value:"proxy_grpc_credentials_key",id:"proxy_grpc_credentials_key",level:4},{value:"proxy_grpc_credentials_value",id:"proxy_grpc_credentials_value",level:4},{value:"GRPC proxy example",id:"grpc-proxy-example",level:3},{value:"Header proxy rules",id:"header-proxy-rules",level:2},{value:"Binary mode",id:"binary-mode",level:2},{value:"Granular proxy mode",id:"granular-proxy-mode",level:2},{value:"Enable granular proxy mode",id:"enable-granular-proxy-mode",level:3},{value:"Defining a list of proxies",id:"defining-a-list-of-proxies",level:3},{value:"Granular connect and refresh",id:"granular-connect-and-refresh",level:3},{value:"Granular subscribe, publish, sub refresh",id:"granular-subscribe-publish-sub-refresh",level:3},{value:"Granular RPC",id:"granular-rpc",level:3}];function a(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"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."}),"\n",(0,r.jsx)(n.p,{children:"The list of events that can be proxied:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"connect"})," \u2013 called when a client connects to Centrifugo, so it's possible to authenticate user, return custom data to a client, subscribe connection to several channels, attach meta information to the connection, and so on. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"refresh"})," - called when a client session is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical connection liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"subscribe"})," - called when clients try to subscribe on a channel, so it's possible to check permissions and return custom initial subscription data. Works for bidirectional transports only."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"publish"})," - called when a client tries to publish into a channel, so it's possible to check permissions and optionally modify publication data. Works for bidirectional transports only."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"sub_refresh"})," - called when a client subscription is going to expire, so it's possible to prolong it or just let it expire. Can also be used just as a periodical subscription liveness callback from Centrifugo to app backend. Works for bidirectional and unidirectional transports."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"rpc"})," - called when a client sends RPC, you can do whatever logic you need based on a client-provided RPC method and params. Works for bidirectional transports only."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"At the moment Centrifugo can proxy these events over two protocols:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"HTTP (JSON payloads)"}),"\n",(0,r.jsx)(n.li,{children:"GRPC (Protobuf messages)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"http-proxy",children:"HTTP proxy"}),"\n",(0,r.jsx)(n.p,{children:"HTTP proxy in Centrifugo converts client connection events into HTTP calls to the application backend."}),"\n",(0,r.jsx)(n.h3,{id:"http-request-structure",children:"HTTP request structure"}),"\n",(0,r.jsxs)(n.p,{children:["All proxy calls are ",(0,r.jsx)(n.strong,{children:"HTTP POST"})," requests that will be sent from Centrifugo to configured endpoints with a configured timeout. These requests will have some headers copied from the original client request (see details below) and include JSON body which varies depending on call type (for example data sent by a client in RPC call etc, see more details about JSON bodies below)."]}),"\n",(0,r.jsx)(n.h3,{id:"proxy-http-headers",children:"Proxy HTTP headers"}),"\n",(0,r.jsx)(n.p,{children:"The good thing about Centrifugo HTTP proxy is that it transparently proxies original HTTP request headers in a request to app backend. In most cases this allows achieving transparent authentication on the application backend side. But it's required to provide an explicit list of HTTP headers you want to be proxied, for example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_http_headers": [\n "Origin",\n "User-Agent",\n "Cookie",\n "Authorization",\n "X-Real-Ip",\n "X-Forwarded-For",\n "X-Request-Id"\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Alternatively, you can set a list of headers via an environment variable (space separated):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'export CENTRIFUGO_PROXY_HTTP_HEADERS="Cookie User-Agent X-B3-TraceId X-B3-SpanId" ./centrifugo\n'})}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Centrifugo forces the",(0,r.jsx)(n.code,{children:" Content-Type"})," header to be ",(0,r.jsx)(n.code,{children:"application/json"})," in all HTTP proxy requests since it sends the body in JSON format to the application backend."]})}),"\n",(0,r.jsx)(n.h3,{id:"proxy-grpc-metadata",children:"Proxy GRPC metadata"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.a,{href:"/docs/4/transports/uni_grpc",children:"GRPC unidirectional stream"})," is used as a client transport then you may want to proxy GRPC metadata from the client request. In this case you may configure ",(0,r.jsx)(n.code,{children:"proxy_grpc_metadata"})," option. This is an array of string metadata keys which will be proxied. These metadata keys transformed to HTTP headers of proxy request. By default no metadata keys are proxied."]}),"\n",(0,r.jsxs)(n.p,{children:["See below ",(0,r.jsx)(n.a,{href:"#header-proxy-rules",children:"the table of rules"})," how metadata and headers proxied in transport/proxy different scenarios."]}),"\n",(0,r.jsx)(n.h3,{id:"connect-proxy",children:"Connect proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_connect_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 connection requests ",(0,r.jsx)(n.strong,{children:"without JWT set"})," will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," URL endpoint. On your backend side, you can authenticate the incoming connection and return client credentials to Centrifugo in response to the proxied request."]}),"\n",(0,r.jsx)(n.admonition,{type:"danger",children:(0,r.jsxs)(n.p,{children:["Make sure you properly configured ",(0,r.jsx)(n.a,{href:"/docs/4/server/configuration#allowed_origins",children:"allowed_origins"})," Centrifugo option or check request origin on your backend side upon receiving connect request from Centrifugo. Otherwise, your site can be vulnerable to CSRF attacks if you are using WebSocket transport for client connections."]})}),"\n",(0,r.jsxs)(n.p,{children:["Yes, this means you don't need to generate JWT and pass it to a client-side and can rely on a cookie while authenticating the user. ",(0,r.jsx)(n.strong,{children:"Centrifugo should work on the same domain in this case so your site cookie could be passed to Centrifugo by browsers"}),". In many cases your existing session mechanism will provide user authentication details to the connect proxy handler."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["If you want to pass some custom authentication token from a client side (not in Centrifugo JWT format) but force request to be proxied then you may put it in a cookie or use connection request custom ",(0,r.jsx)(n.code,{children:"data"})," field (available in all our transports). This ",(0,r.jsx)(n.code,{children:"data"})," can contain arbitrary payload you want to pass from a client to a server."]})}),"\n",(0,r.jsxs)(n.p,{children:["This also means that ",(0,r.jsx)(n.strong,{children:"every"})," new connection from a user will result in an HTTP POST request to your application backend. While with JWT token you usually generate it once on application page reload, if client reconnects due to Centrifugo restart or internet connection loss it uses the same JWT it had before thus usually no additional requests are generated during reconnect process (until JWT expired)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(70160).Z+"",width:"2600",height:"1032"})}),"\n",(0,r.jsxs)(n.p,{children:["Payload example that will be sent to app backend when client without token wants to establish a connection with Centrifugo and ",(0,r.jsx)(n.code,{children:"proxy_connect_endpoint"})," is set to non-empty URL string:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"user": "56"}}\n'})}),"\n",(0,r.jsx)(n.p,{children:"This response allows connecting and tells Centrifugo the ID of a user. See below the full list of supported fields in the result."}),"\n",(0,r.jsx)(n.p,{children:"Several app examples which use connect proxy can be found in our blog:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"With NodeJS"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/11/04/integrating-with-django-building-chat-application",children:"With Django"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.a,{href:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",children:"With Laravel"})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"connect-request-fields",children:"Connect request fields"}),"\n",(0,r.jsx)(n.p,{children:"This is what sent from Centrifugo to application backend in case of connect proxy request."}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional name of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"version"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional version of the client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from client (this field will only be set if provided by a client on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional data from the client in base64 format (if the binary proxy mode is used)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"Array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"list of server-side channels client want to subscribe to, the application server must check permissions and add allowed channels to result"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"connect-result-fields",children:"Connect result fields"}),"\n",(0,r.jsxs)(n.p,{children:["This is what application returns to Centrifugo inside ",(0,r.jsx)(n.code,{children:"result"})," field in case of connect proxy request."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"user ID (calculated on app backend based on request cookie header for example). Return it as an empty string for accepting unauthenticated requests"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a timestamp when connection must be considered expired. If not set or set to ",(0,r.jsx)(n.code,{children:"0"})," connection won't expire at all"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in connect command response."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in the connect command response for binary connections, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channels"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["allows providing a list of server-side channels to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"subs"}),(0,r.jsx)(n.td,{children:"map of SubscribeOptions"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["map of channels with options to subscribe connection to. See more details about ",(0,r.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsxs)(n.td,{children:["JSON object (ex. ",(0,r.jsx)(n.code,{children:'{"key": "value"}'}),")"]}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a custom data to attach to connection (this ",(0,r.jsx)(n.strong,{children:"won't be exposed to client-side"}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_connect_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h4,{id:"example",children:"Example"}),"\n",(0,r.jsxs)(n.p,{children:["Here is the simplest example of the connect handler in Tornado Python framework (note that in a real system you need to authenticate the user on your backend side, here we just return ",(0,r.jsx)(n.code,{children:'"56"'})," as user ID):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"class CentrifugoConnectHandler(tornado.web.RequestHandler):\n\n def check_xsrf_cookie(self):\n pass\n\n def post(self):\n self.set_header('Content-Type', 'application/json; charset=\"utf-8\"')\n data = json.dumps({\n 'result': {\n 'user': '56'\n }\n })\n self.write(data)\n\n\ndef main():\n options.parse_command_line()\n app = tornado.web.Application([\n (r'/centrifugo/connect', CentrifugoConnectHandler),\n ])\n app.listen(3000)\n tornado.ioloop.IOLoop.instance().start()\n\n\nif __name__ == '__main__':\n main()\n"})}),"\n",(0,r.jsx)(n.p,{children:"This example should help you to implement a similar HTTP handler in any language/framework you are using on the backend side."}),"\n",(0,r.jsxs)(n.p,{children:["We also have a tutorial in the blog about ",(0,r.jsx)(n.a,{href:"/blog/2021/10/18/integrating-with-nodejs",children:"Centrifugo integration with NodeJS"})," which uses connect proxy and native session middleware of Express.js to authenticate connections. Even if you are not using NodeJS on a backend a tutorial can help you understand the idea."]}),"\n",(0,r.jsx)(n.h4,{id:"what-if-connection-is-unauthenticatedunauthorized-to-connect",children:"What if connection is unauthenticated/unauthorized to connect?"}),"\n",(0,r.jsxs)(n.p,{children:["In this case return a disconnect object as a response. See ",(0,r.jsx)(n.a,{href:"#return-custom-disconnect",children:"Return custom disconnect"})," section. Depending on whether you want connection to reconnect or not (usually not) you can select the appropriate disconnect code. Sth like this in response:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4501,\n "reason": "unauthorized"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 may be sufficient enough. Choosing codes and reason is up to the developer, but follow the rules described in ",(0,r.jsx)(n.a,{href:"#return-custom-disconnect",children:"Return custom disconnect"})," section."]}),"\n",(0,r.jsx)(n.h3,{id:"refresh-proxy",children:"Refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_refresh_endpoint": "http://localhost:3000/centrifugo/refresh",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_refresh_endpoint"})," when it's time to refresh the connection. Centrifugo itself will ask your backend about connection validity instead of refresh workflow on the client-side."]}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in refresh request (when the connection is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"refresh-request-fields",children:"Refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"refresh-result-fields",children:"Refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a connection info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary connection info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-1",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_refresh_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"rpc-proxy",children:"RPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_rpc_endpoint": "http://localhost:3000/centrifugo/connect",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["RPC calls over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_rpc_endpoint"}),". This allows a developer to utilize WebSocket (or SockJS) connection in a bidirectional way."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in RPC request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "method": "getCurrentPrice",\n "data":{"params": {"object_id": 12}}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"data": {"answer": "2019"}}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"rpc-request-fields",children:"RPC request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"method"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"an RPC method string, if the client does not use named RPC call then method will be omitted"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC custom data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"rpc-result-fields",children:"RPC result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"RPC response - any valid JSON is supported"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["can be set instead of ",(0,r.jsx)(n.code,{children:"data"})," for binary response encoded in base64 format"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-2",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_rpc_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return a custom error."}),"\n",(0,r.jsx)(n.h3,{id:"subscribe-proxy",children:"Subscribe proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 subscribe requests sent over client connection will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_subscribe_endpoint"}),". This allows you to check the access of the client to a channel."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:[(0,r.jsxs)(n.strong,{children:["Subscribe proxy does not proxy ",(0,r.jsx)(n.a,{href:"/docs/4/server/channels#private-channel-prefix",children:"private"})," and ",(0,r.jsx)(n.a,{href:"/docs/4/server/channels#user-channel-boundary",children:"user-limited"})," channels at the moment"]}),". That's because those are already providing a level of security (user-limited channels check current user ID, private channels require subscription token). In some cases you may use subscribe proxy as a replacement for private channels actually: if you prefer to check permissions using the proxy to backend mechanism \u2013 just stop using ",(0,r.jsx)(n.code,{children:"$"})," prefixes in channels, properly configure subscribe proxy and validate subscriptions upon proxy from Centrifugo to your backend (issued each time user tries to subscribe on a channel for which subscribe proxy enabled)."]})}),"\n",(0,r.jsxs)(n.p,{children:["Unlike proxy types described above subscribe proxy must be enabled per channel namespace. This means that every namespace (including global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," that enables subscribe proxy for channels in a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable subscribe proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_subscribe"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_subscribe_endpoint": "http://localhost:3000/centrifugo/subscribe",\n "proxy_subscribe_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "proxy_subscribe": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in subscribe request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if subscription is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-request-fields",children:"Subscribe request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"})," or ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to subscribe to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"custom data from client sent with subscription request (this field will only be set if provided by a client on subscribe)."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"optional subscription data from the client in base64 format (if the binary proxy mode is used)."})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"subscribe-result-fields",children:"Subscribe result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary connection channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a custom data to send to the client in subscribe command reply, will be decoded to raw bytes on Centrifugo side before sending to client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"override"}),(0,r.jsx)(n.td,{children:"Override object"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Allows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"presence"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override presence"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_push_join_leave"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_push_join_leave"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_positioning"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_positioning"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"force_recovery"}),(0,r.jsx)(n.td,{children:"BoolValue"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"Override force_recovery"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow subscribing."}),"\n",(0,r.jsx)(n.h4,{id:"options-3",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_subscribe_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h4,{id:"what-if-connection-is-not-allowed-to-subscribe",children:"What if connection is not allowed to subscribe?"}),"\n",(0,r.jsxs)(n.p,{children:["In this case you can return error object as a subscribe handler response. See ",(0,r.jsx)(n.a,{href:"#return-custom-error",children:"return custom error"})," section."]}),"\n",(0,r.jsx)(n.p,{children:"In general, frontend applications should not try to subscribe to channels for which access is not allowed. But these situations can happen or malicious user can try to subscribe to a channel. In most scenarios returning:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 403,\n "message": "permission denied"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 is sufficient enough. Error code may be not 403 actually, no real reason to force HTTP semantics here - so it's up to Centrifugo user to decide. Just keep it in range [400, 1999] as described ",(0,r.jsx)(n.a,{href:"#return-custom-error",children:"here"}),"."]}),"\n",(0,r.jsxs)(n.p,{children:["If case of returning response above, on client side ",(0,r.jsx)(n.code,{children:"unsubscribed"})," event of Subscription object will be called with error code 403. Subscription won't resubscribe automatically after that."]}),"\n",(0,r.jsx)(n.h3,{id:"publish-proxy",children:"Publish proxy"}),"\n",(0,r.jsx)(n.p,{children:"With the following option in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 publish calls sent by a client will be proxied to ",(0,r.jsx)(n.code,{children:"proxy_publish_endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"This request happens BEFORE a message is published to a channel, so your backend can validate whether a client can publish data to a channel. An important thing here is that publication to the channel can fail after your backend successfully validated publish request (for example publish to Redis by Centrifugo returned an error). In this case, your backend won't know about the error that happened but this error will propagate to the client-side."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{src:s(97172).Z+"",width:"2600",height:"1098"})}),"\n",(0,r.jsxs)(n.p,{children:["Like the subscribe proxy, publish proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_publish"})," that enables publish proxy for channels in the namespace. All other namespace options will be taken into account before making a proxy request, so you also need to turn on the ",(0,r.jsx)(n.code,{children:"publish"})," option too."]}),"\n",(0,r.jsxs)(n.p,{children:["So to enable publish proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_publish"})," and ",(0,r.jsx)(n.code,{children:"publish"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "publish": true,\n "proxy_publish": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_publish_endpoint": "http://localhost:3000/centrifugo/publish",\n "proxy_publish_timeout": "1s",\n "namespaces": [{\n "name": "sun",\n "publish": true,\n "proxy_publish": true\n }]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Keep in mind that this will only work if the ",(0,r.jsx)(n.code,{children:"publish"})," channel option is on for a channel namespace (or for a global top-level namespace)."]}),"\n",(0,r.jsx)(n.p,{children:"Payload example sent to app backend in a publish request:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "chat:index",\n "data":{"input":"hello"}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example if publish is allowed:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"publish-request-fields",children:"Publish request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by the client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a string channel client wants to publish to"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"data sent by client"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["will be set instead of ",(0,r.jsx)(n.code,{children:"data"})," field for binary proxy mode"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"publish-result-fields",children:"Publish result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"data"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["an optional JSON data to send into a channel ",(0,r.jsx)(n.strong,{children:"instead of"})," original data sent by a client"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64data"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a binary data encoded in base64 format, the meaning is the same as for data above, will be decoded to raw bytes on Centrifugo side before publishing"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"skip_history"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["when set to ",(0,r.jsx)(n.code,{children:"true"})," Centrifugo won't save publication to the channel history"]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"See below on how to return an error in case you don't want to allow publishing."}),"\n",(0,r.jsx)(n.h4,{id:"options-4",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_publish_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"sub-refresh-proxy",children:"Sub refresh proxy"}),"\n",(0,r.jsx)(n.p,{children:"Added in Centrifugo v4.1.1"}),"\n",(0,r.jsx)(n.p,{children:"With the following options in the configuration file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/sub_refresh",\n "proxy_sub_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["\u2013 Centrifugo will call ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh_endpoint"})," when it's time to refresh the subscription. Centrifugo itself will ask your backend about subscription validity instead of subscription refresh workflow on the client-side."]}),"\n",(0,r.jsxs)(n.p,{children:["Like subscribe and publish proxy types, sub refresh proxy must be enabled per channel namespace. This means that every namespace (including the global/default one) has a boolean option ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh"})," that enables sub refresh proxy for channels in the namespace. Only subscriptions which have expiration time will be validated over sub refresh proxy endpoint."]}),"\n",(0,r.jsx)(n.p,{children:"Sub refresh proxy may be used as a periodical Subscription liveness callback from Centrifugo to app backend."}),"\n",(0,r.jsx)(n.admonition,{type:"caution",children:(0,r.jsx)(n.p,{children:"In the current implementation the delay of Subscription refresh requests from Centrifugo to application backend may be up to one minute (was implemented this way from a simplicity and efficiency perspective). We assume this should be enough for many scenarios. But this may be improved if needed. Please reach us out with a detailed description of your use case where you want more accurate requests to refresh subscriptions."})}),"\n",(0,r.jsxs)(n.p,{children:["So to enable sub refresh proxy for channels without namespace define ",(0,r.jsx)(n.code,{children:"proxy_sub_refresh"})," on a top configuration level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/sub_refresh",\n "proxy_sub_refresh": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Or for channels in namespace ",(0,r.jsx)(n.code,{children:"sun"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n ...\n "proxy_sub_refresh_endpoint": "http://localhost:3000/centrifugo/publish",\n "namespaces": [{\n "name": "sun",\n "proxy_sub_refresh": true\n }]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"The payload sent to app backend in sub refresh request (when the subscription is going to expire):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "client":"9336a229-2400-4ebc-8c50-0a643d22e8a0",\n "transport":"websocket",\n "protocol": "json",\n "encoding":"json",\n "user":"56",\n "channel": "channel"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Expected response example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"result": {"expire_at": 1565436268}}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"sub-refresh-request-fields",children:"Sub refresh request fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"client"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"unique client ID generated by Centrifugo for each incoming connection"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"transport"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["transport name (ex. ",(0,r.jsx)(n.code,{children:"websocket"}),", ",(0,r.jsx)(n.code,{children:"sockjs"}),", ",(0,r.jsx)(n.code,{children:"uni_sse"})," etc.)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"protocol"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol type used by client (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"protobuf"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"encoding"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["protocol encoding type used (",(0,r.jsx)(n.code,{children:"json"})," or ",(0,r.jsx)(n.code,{children:"binary"})," at moment)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"user"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"a connection user ID obtained during authentication process"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"channel"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"channel for which Subscription is going to expire"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"meta"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["a connection attached meta (off by default, enable with ",(0,r.jsx)(n.code,{children:'"proxy_include_connection_meta": true'}),")"]})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"sub-refresh-result-fields",children:"Sub refresh result fields"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Optional"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expired"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a flag to mark the connection as expired - the client will be disconnected"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"expire_at"}),(0,r.jsx)(n.td,{children:"integer"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a timestamp in the future when connection must be considered expired"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"info"}),(0,r.jsx)(n.td,{children:"JSON"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"a channel info JSON"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"b64info"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsx)(n.td,{children:"binary channel info encoded in base64 format, will be decoded to raw bytes on Centrifugo before using in messages"})]})]})]}),"\n",(0,r.jsx)(n.h4,{id:"options-5",children:"Options"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"proxy_sub_refresh_timeout"})," (duration) config option controls timeout of HTTP POST request sent to app backend. By default ",(0,r.jsx)(n.code,{children:"1s"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-error",children:"Return custom error"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains an error to return it to the client:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": 1000,\n "message": "custom error"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Applications ",(0,r.jsx)(n.strong,{children:"must use error codes in range [400, 1999]"}),". Error code field is ",(0,r.jsx)(n.code,{children:"uint32"})," internally."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsx)(n.p,{children:"Returning custom error does not apply to response for refresh and sub refresh proxy requests as there is no sense in returning an error (will not reach client anyway)."})}),"\n",(0,r.jsx)(n.h3,{id:"return-custom-disconnect",children:"Return custom disconnect"}),"\n",(0,r.jsx)(n.p,{children:"Application backend can return JSON object that contains a custom disconnect object to disconnect client in a custom way:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "disconnect": {\n "code": 4500,\n "reason": "disconnect reason"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Application ",(0,r.jsx)(n.strong,{children:"must use numbers in the range 4000-4999 for custom disconnect codes"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"codes in range [4000, 4499] give client an advice to reconnect"}),"\n",(0,r.jsx)(n.li,{children:"codes in range [4500, 4999] are terminal codes \u2013 client won't reconnect upon receiving it."}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Code is ",(0,r.jsx)(n.code,{children:"uint32"})," internally. Numbers outside of 4000-4999 range are reserved by Centrifugo internal protocol. Keep in mind that ",(0,r.jsx)(n.strong,{children:"due to WebSocket protocol limitations and Centrifugo internal protocol needs you need to keep disconnect reason string no longer than 32 ASCII symbols (i.e. 32 bytes max)"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"note",children:(0,r.jsxs)(n.p,{children:["Returning custom disconnect does not apply to response for refresh and sub refresh proxy requests as there is no way to control disconnect at moment - the client will always be disconnected with ",(0,r.jsx)(n.code,{children:"expired"})," disconnect reason."]})}),"\n",(0,r.jsx)(n.h2,{id:"grpc-proxy",children:"GRPC proxy"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can also proxy connection events to your backend over GRPC instead of HTTP. In this case, Centrifugo acts as a GRPC client and your backend acts as a GRPC server."}),"\n",(0,r.jsxs)(n.p,{children:["GRPC service definitions can be found in the Centrifugo repository: ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/blob/master/internal/proxyproto/proxy.proto",children:"proxy.proto"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"GRPC proxy inherits all the fields for HTTP proxy \u2013 so you can refer to field descriptions for HTTP above. Both proxy types in Centrifugo share the same Protobuf schema definitions."})}),"\n",(0,r.jsx)(n.p,{children:"Every proxy call in this case is a unary GRPC call. Centrifugo puts client headers into GRPC metadata (since GRPC doesn't have headers concept)."}),"\n",(0,r.jsxs)(n.p,{children:["All you need to do to enable proxying over GRPC instead of HTTP is to use ",(0,r.jsx)(n.code,{children:"grpc"})," schema in endpoint, for example for the connect proxy:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_connect_endpoint": "grpc://localhost:12000",\n "proxy_connect_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_refresh_endpoint": "grpc://localhost:12000",\n "proxy_refresh_timeout": "1s"\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or for RPC proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_rpc_endpoint": "grpc://localhost:12000",\n "proxy_rpc_timeout": "1s"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["For publish proxy in namespace ",(0,r.jsx)(n.code,{children:"chat"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_publish_endpoint": "grpc://localhost:12000",\n "proxy_publish_timeout": "1s"\n "namespaces": [\n {\n "name": "chat",\n "publish": true,\n "proxy_publish": true\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"Use subscribe proxy for all channels without namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_subscribe_endpoint": "grpc://localhost:12000",\n "proxy_subscribe_timeout": "1s",\n "proxy_subscribe": true\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:"So the same as for HTTP, just the different endpoint scheme."}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-options",children:"GRPC proxy options"}),"\n",(0,r.jsx)(n.p,{children:"Some additional options exist to control GRPC proxy behavior."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_cert_file",children:"proxy_grpc_cert_file"}),"\n",(0,r.jsxs)(n.p,{children:["String, default: ",(0,r.jsx)(n.code,{children:'""'}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_key",children:"proxy_grpc_credentials_key"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsx)(n.p,{children:"Add custom key to per-RPC credentials."}),"\n",(0,r.jsx)(n.h4,{id:"proxy_grpc_credentials_value",children:"proxy_grpc_credentials_value"}),"\n",(0,r.jsxs)(n.p,{children:["String, default ",(0,r.jsx)(n.code,{children:'""'})," (i.e. not used)."]}),"\n",(0,r.jsxs)(n.p,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"proxy_grpc_credentials_key"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"grpc-proxy-example",children:"GRPC proxy example"}),"\n",(0,r.jsxs)(n.p,{children:["We have ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/go_proxy/grpc",children:"an example of backend server"})," (written in Go language) which can react to events from Centrifugo over GRPC. For other programming languages the approach is similar, i.e.:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Copy proxy Protobuf definitions"}),"\n",(0,r.jsx)(n.li,{children:"Generate GRPC code"}),"\n",(0,r.jsx)(n.li,{children:"Run backend service with you custom business logic"}),"\n",(0,r.jsx)(n.li,{children:"Point Centrifugo to it."}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"header-proxy-rules",children:"Header proxy rules"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo not only supports HTTP-based client transports but also GRPC-based (for example GRPC unidirectional stream). Here is a table with rules used to proxy headers/metadata in various scenarios:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Client protocol type"}),(0,r.jsx)(n.th,{children:"Proxy type"}),(0,r.jsx)(n.th,{children:"Client headers"}),(0,r.jsx)(n.th,{children:"Client metadata"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"In proxy request headers"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"In proxy request metadata"}),(0,r.jsx)(n.td,{children:"N/A"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GRPC"}),(0,r.jsx)(n.td,{children:"HTTP"}),(0,r.jsx)(n.td,{children:"N/A"}),(0,r.jsx)(n.td,{children:"In proxy request headers"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"binary-mode",children:"Binary mode"}),"\n",(0,r.jsxs)(n.p,{children:["As you may noticed there are several fields in request/result description of various proxy calls which use ",(0,r.jsx)(n.code,{children:"base64"})," encoding."]}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo can work with binary Protobuf protocol (in case of bidirectional WebSocket transport). All our bidirectional clients support this."}),"\n",(0,r.jsx)(n.p,{children:"Most Centrifugo users use JSON for custom payloads: i.e. for data sent to a channel, for connection info attached while authenticating (which becomes part of presence response, join/leave messages and added to Publication client info when message published from a client side)."}),"\n",(0,r.jsx)(n.p,{children:"But since HTTP proxy works with JSON format (i.e. sends requests with JSON body) \u2013 it can not properly pass binary data to application backend. Arbitrary binary data can't be encoded into JSON."}),"\n",(0,r.jsx)(n.p,{children:"In this case it's possible to turn Centrifugo proxy into binary mode by using:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "proxy_binary_encoding": true\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Once enabled this option tells Centrifugo to use base64 format in requests and utilize fields like ",(0,r.jsx)(n.code,{children:"b64data"}),", ",(0,r.jsx)(n.code,{children:"b64info"})," with payloads encoded to base64 instead of their JSON field analogues."]}),"\n",(0,r.jsx)(n.p,{children:"While this feature is useful for HTTP proxy it's not really required if you are using GRPC proxy \u2013 since GRPC allows passing binary data just fine."}),"\n",(0,r.jsx)(n.p,{children:"Regarding b64 fields in proxy results \u2013 just use base64 fields when required \u2013 Centrifugo is smart enough to detect that you are using base64 field and will pick payload from it, decode from base64 automatically and will pass further to connections in binary format."}),"\n",(0,r.jsx)(n.h2,{id:"granular-proxy-mode",children:"Granular proxy mode"}),"\n",(0,r.jsx)(n.p,{children:"By default, with proxy configuration shown above, you can only define a global proxy settings and one endpoint for each type of proxy (i.e. one for connect proxy, one for subscribe proxy, and so on). Also, you can configure only one set of headers to proxy which will be used by each proxy type. This may be sufficient for many use cases, but what if you need a more granular control? For example, use different subscribe proxy endpoints for different channel namespaces (i.e. when using microservice architecture)."}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo v3.1.0 introduced a new mode for proxy configuration called granular proxy mode. In this mode it's possible to configure subscribe and publish proxy behaviour on per-namespace level, use different set of headers passed to the proxy endpoint in each proxy type. Also, Centrifugo v3.1.0 introduced a concept of rpc namespaces (in addition to channel namespaces) \u2013 together with granular proxy mode this allows configuring rpc proxies on per rpc namespace basis."}),"\n",(0,r.jsx)(n.h3,{id:"enable-granular-proxy-mode",children:"Enable granular proxy mode"}),"\n",(0,r.jsxs)(n.p,{children:["Since the change is rather radical it requires a separate boolean option ",(0,r.jsx)(n.code,{children:"granular_proxy_mode"})," to be enabled. As soon as this option set Centrifugo does not use proxy configuration rules described above and follows the rules described below."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"defining-a-list-of-proxies",children:"Defining a list of proxies"}),"\n",(0,r.jsxs)(n.p,{children:["When using granular proxy mode on configuration top level you can define ",(0,r.jsx)(n.code,{children:'"proxies"'})," array with a list of different proxy objects. Each proxy object in an array should have at least two required fields: ",(0,r.jsx)(n.code,{children:"name"})," and ",(0,r.jsx)(n.code,{children:"endpoint"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Here is an example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [\n {\n "name": "connect",\n "endpoint": "http://localhost:3000/centrifugo/connect",\n "timeout": "500ms",\n "http_headers": ["Cookie"]\n },\n {\n "name": "refresh",\n "endpoint": "http://localhost:3000/centrifugo/refresh",\n "timeout": "500ms"\n },\n {\n "name": "subscribe1",\n "endpoint": "http://localhost:3001/centrifugo/subscribe"\n },\n {\n "name": "publish1",\n "endpoint": "http://localhost:3001/centrifugo/publish"\n },\n {\n "name": "rpc1",\n "endpoint": "http://localhost:3001/centrifugo/rpc"\n },\n {\n "name": "subscribe2",\n "endpoint": "http://localhost:3002/centrifugo/subscribe"\n },\n {\n "name": "publish2",\n "endpoint": "grpc://localhost:3002"\n }\n {\n "name": "rpc2",\n "endpoint": "grpc://localhost:3002"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Let's look at all fields for a proxy object which is possible to set for each proxy inside ",(0,r.jsx)(n.code,{children:'"proxies"'})," array."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field name"}),(0,r.jsx)(n.th,{children:"Field type"}),(0,r.jsx)(n.th,{children:"Required"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"name"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["Unique name of proxy used for referencing in configuration, must match regexp ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"endpoint"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"yes"}),(0,r.jsxs)(n.td,{children:["HTTP or GRPC endpoint in the same format as in default proxy mode. For example, ",(0,r.jsx)(n.code,{children:"http://localhost:3000/path"})," for HTTP or ",(0,r.jsx)(n.code,{children:"grpc://localhost:3000"})," for GRPC."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"timeout"}),(0,r.jsx)(n.td,{children:"duration (string)"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["Proxy request timeout, default ",(0,r.jsx)(n.code,{children:'"1s"'})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"http_headers"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of headers to proxy, by default no headers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_metadata"}),(0,r.jsx)(n.td,{children:"array of strings"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"List of GRPC metadata keys to proxy, by default no metadata keys"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"binary_encoding"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Use base64 for payloads"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"include_connection_meta"}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Include meta information (attached on connect)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_cert_file"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Path to cert file for secure TLS connection. If not set then an insecure connection with the backend endpoint is used."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_key"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsx)(n.td,{children:"Add custom key to per-RPC credentials."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"grpc_credentials_value"}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"no"}),(0,r.jsxs)(n.td,{children:["A custom value for ",(0,r.jsx)(n.code,{children:"grpc_credentials_key"}),"."]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"granular-connect-and-refresh",children:"Granular connect and refresh"}),"\n",(0,r.jsx)(n.p,{children:"As soon as you defined a list of proxies you can reference them by a name to use a specific proxy configuration for a specific event."}),"\n",(0,r.jsx)(n.p,{children:"To enable connect proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect"\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["We have an ",(0,r.jsx)(n.a,{href:"https://github.com/centrifugal/examples/tree/master/v3/nodejs_granular_proxy",children:"example of Centrifugo integration with NodeJS"})," which uses granular proxy mode. Even if you are not using NodeJS on a backend an example can help you understand the idea."]}),"\n",(0,r.jsx)(n.p,{children:"Let's also add refresh proxy:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "connect_proxy_name": "connect",\n "refresh_proxy_name": "refresh"\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"granular-subscribe-publish-sub-refresh",children:"Granular subscribe, publish, sub refresh"}),"\n",(0,r.jsxs)(n.p,{children:["Subscribe, publish and sub refresh proxies work per-namespace. This means that ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"publish_proxy_name"})," and ",(0,r.jsx)(n.code,{children:"sub_refresh_proxy_name"})," are just channel namespace options. So it's possible to define these options on configuration top-level (for channels in default top-level namespace) or inside namespace object."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [\n {\n "name": "ns1",\n "subscribe_proxy_name": "subscribe1",\n "publish": true,\n "publish_proxy_name": "publish1"\n },\n {\n "name": "ns2",\n "subscribe_proxy_name": "subscribe2",\n "publish": true,\n "publish_proxy_name": "publish2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"subscribe_proxy_name"'})," is empty then no subscribe proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"publish_proxy_name"'})," is empty then no publish proxy will be used for a namespace."]}),"\n",(0,r.jsxs)(n.p,{children:["If namespace does not have ",(0,r.jsx)(n.code,{children:'"sub_refresh_proxy_name"'})," or ",(0,r.jsx)(n.code,{children:'"sub_refresh_proxy_name"'})," is empty then no sub refresh proxy will be used for a namespace."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["You can define ",(0,r.jsx)(n.code,{children:"subscribe_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"publish_proxy_name"}),", ",(0,r.jsx)(n.code,{children:"sub_refresh_proxy_name"})," on configuration top level \u2013 and in this case publish, subscribe and sub refresh requests for channels without explicit namespace will be proxied using this proxy. The same mechanics as for other channel options in Centrifugo."]})}),"\n",(0,r.jsx)(n.h3,{id:"granular-rpc",children:"Granular RPC"}),"\n",(0,r.jsx)(n.p,{children:"Analogous to channel namespaces it's possible to configure rpc namespaces:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "granular_proxy_mode": true,\n "proxies": [...],\n "namespaces": [...],\n "rpc_namespaces": [\n {\n "name": "rpc_ns1",\n "rpc_proxy_name": "rpc1",\n },\n {\n "name": "rpc_ns2",\n "rpc_proxy_name": "rpc2"\n }\n ]\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The mechanics is the same as for channel namespaces. RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns1:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc1"}),", RPC requests with RPC method like ",(0,r.jsx)(n.code,{children:"rpc_ns2:test"})," will use rpc proxy ",(0,r.jsx)(n.code,{children:"rpc2"}),". So Centrifugo uses ",(0,r.jsx)(n.code,{children:":"})," as RPC namespace boundary in RPC method (just like it does for channel namespaces)."]}),"\n",(0,r.jsxs)(n.p,{children:["Just like channel namespaces RPC namespaces should have a name which match ",(0,r.jsx)(n.code,{children:"^[-a-zA-Z0-9_.]{2,}$"})," regexp pattern \u2013 this is validated on Centrifugo start."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["The same as for channel namespaces and channel options you can define ",(0,r.jsx)(n.code,{children:"rpc_proxy_name"})," on configuration top level \u2013 and in this case RPC calls without explicit namespace in RPC method will be proxied using this proxy."]})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},70160:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_connect_proxy-4318d8beb2c7553d9b30b2ed7fb8edac.png"},97172:(e,n,s)=>{s.d(n,{Z:()=>r});const r=s.p+"assets/images/diagram_publish_proxy-66ccb1e8b37ed8912d218b4529597bd9.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>c,a:()=>o});var r=s(67294);const i={},t=r.createContext(i);function o(e){const n=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/49012ebf.753e03a1.js b/assets/js/49012ebf.82ed41cd.js similarity index 56% rename from assets/js/49012ebf.753e03a1.js rename to assets/js/49012ebf.82ed41cd.js index ab5473aaf..67db4488b 100644 --- a/assets/js/49012ebf.753e03a1.js +++ b/assets/js/49012ebf.82ed41cd.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9604],{19036:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=i(85893),s=i(11151);const r={id:"analytics",title:"Analytics with ClickHouse"},o=void 0,c={id:"pro/analytics",title:"Analytics with ClickHouse",description:"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.",source:"@site/versioned_docs/version-4/pro/analytics.md",sourceDirName:"pro",slug:"/pro/analytics",permalink:"/docs/4/pro/analytics",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/analytics.md",tags:[],version:"4",frontMatter:{id:"analytics",title:"Analytics with ClickHouse"},sidebar:"Pro",previous:{title:"User and channel tracing",permalink:"/docs/4/pro/tracing"},next:{title:"Operation throttling",permalink:"/docs/4/pro/throttling"}},a={},l=[{value:"Configuration",id:"configuration",level:2},{value:"Connections table",id:"connections-table",level:2},{value:"Subscriptions table",id:"subscriptions-table",level:2},{value:"Operations table",id:"operations-table",level:2},{value:"Publications table",id:"publications-table",level:2},{value:"Notifications table",id:"notifications-table",level:2},{value:"Query examples",id:"query-examples",level:2},{value:"Development",id:"development",level:2},{value:"How export works",id:"how-export-works",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ",(0,t.jsx)(n.a,{href:"https://clickhouse.com/",children:"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."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"clickhouse",src:i(14323).Z+"",width:"3346",height:"1067"})}),"\n",(0,t.jsx)(n.p,{children:"This unlocks a great observability and a way to perform various analytics queries for better connection behavior understanding, check application correctness, building trends, reports, and so on."}),"\n",(0,t.jsx)(n.p,{children:"As soon as you start using integration with ClickHouse some of mentioned possibilities may be easily accessed with Centrifugo PRO web UI and it's analytics page:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Admin analytics",src:i(37508).Z+"",width:"2854",height:"1396"})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n.p,{children:"To enable integration with ClickHouse add the following section to a configuration file:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000",\n "tcp://127.0.0.1:9001",\n "tcp://127.0.0.1:9002",\n "tcp://127.0.0.1:9003"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "centrifugo_cluster",\n "export_connections": true,\n "export_subscriptions": true,\n "export_operations": true,\n "export_publications": true,\n "export_notifications": true,\n "export_http_headers": [\n "User-Agent",\n "Origin",\n "X-Real-Ip"\n ]\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All ClickHouse analytics options scoped to ",(0,t.jsx)(n.code,{children:"clickhouse_analytics"})," section of configuration."]}),"\n",(0,t.jsxs)(n.p,{children:["Toggle this feature using ",(0,t.jsx)(n.code,{children:"enabled"})," boolean option."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["While we have a nested configuration here it's still possible to use environment variables to set options. For example, use ",(0,t.jsx)(n.code,{children:"CENTRIFUGO_CLICKHOUSE_ANALYTICS_ENABLED"})," env var name for configure ",(0,t.jsx)(n.code,{children:"enabled"})," option mentioned above. I.e. nesting expressed as ",(0,t.jsx)(n.code,{children:"_"})," in Centrifugo."]})}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo can export data to different ClickHouse instances, addresses of ClickHouse can be set over ",(0,t.jsx)(n.code,{children:"clickhouse_dsn"})," option."]}),"\n",(0,t.jsxs)(n.p,{children:["You also need to set a ClickHouse cluster name (",(0,t.jsx)(n.code,{children:"clickhouse_cluster"}),") and database name ",(0,t.jsx)(n.code,{children:"clickhouse_database"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_connections"})," tells Centrifugo to export connection information snapshots. Information about connection will be exported once a connection established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_subscriptions"})," tells Centrifugo to export subscription information snapshots. Information about subscription will be exported once a subscription established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_operations"})," tells Centrifugo to export individual client operation information. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_publications"})," tells Centrifugo to export publications for channels to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_notifications"})," tells Centrifugo to export push notifications to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_http_headers"})," is a list of HTTP headers to export for connection information."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_grpc_metadata"})," is a list of metadata keys to export for connection information for GRPC unidirectional transport."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_schema_initialization"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". By default Centrifugo tries to initialize table schema on start (if not exists). This flag allows skipping initialization process."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_ping_on_start"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". Centrifugo pings Clickhouse servers by default on start, if any of servers is unavailable \u2013 Centrifugo fails to start. This option allow skipping this check thus Centrifugo is able to start even if Clickhouse cluster not working correctly."]}),"\n",(0,t.jsx)(n.h2,{id:"connections-table",children:"Connections table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/connections', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections_distributed\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'connections', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"subscriptions-table",children:"Subscriptions table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions_distributed\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'subscriptions', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"operations-table",children:"Operations table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/operations', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'operations', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"publications-table",children:"Publications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.publications\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'publications', murmurHash3_64(channel)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"notifications-table",children:"Notifications table"}),"\n",(0,t.jsxs)(n.p,{children:["\ud83d\udea7 This PRO feature is under construction together with ",(0,t.jsx)(n.a,{href:"/docs/4/pro/push_notifications",children:"push notification API"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.notifications\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'notifications', murmurHash3_64(uid)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"query-examples",children:"Query examples"}),"\n",(0,t.jsx)(n.p,{children:"Show unique users which were connected:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT DISTINCT user\nFROM centrifugo.connections_distributed;\n\n\u250c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 user_1 \u2502\n\u2502 user_2 \u2502\n\u2502 user_3 \u2502\n\u2502 user_4 \u2502\n\u2502 user_5 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show total number of publication attempts which were throttled by Centrifugo (received ",(0,t.jsx)(n.code,{children:"Too many requests"})," error with code ",(0,t.jsx)(n.code,{children:"111"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 4502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"The same for a specific user:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish') AND (user = 'user_200');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 1214 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show number of unique users subscribed to a specific channel in last 5 minutes (this is approximate since subscriptions table contain periodic snapshot entries, clients could unsubscribe in between snapshots \u2013 this is reflected in operations table):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(Distinct(user))\nFROM centrifugo.subscriptions_distributed\nWHERE arrayExists(x -> (x = 'chat:index'), channels) AND (time >= (now() - toIntervalMinute(5)));\n\n\u250c\u2500uniqExact(user)\u2500\u2510\n\u2502 101 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show top 10 users which called ",(0,t.jsx)(n.code,{children:"publish"})," operation during last one minute:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT\n COUNT(op) AS num_ops,\n user\nFROM centrifugo.operations_distributed\nWHERE (op = 'publish') AND (time >= (now() - toIntervalMinute(1)))\nGROUP BY user\nORDER BY num_ops DESC\nLIMIT 10;\n\n\u250c\u2500num_ops\u2500\u252c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 56 \u2502 user_200 \u2502\n\u2502 11 \u2502 user_75 \u2502\n\u2502 6 \u2502 user_87 \u2502\n\u2502 6 \u2502 user_65 \u2502\n\u2502 6 \u2502 user_39 \u2502\n\u2502 5 \u2502 user_28 \u2502\n\u2502 5 \u2502 user_63 \u2502\n\u2502 5 \u2502 user_89 \u2502\n\u2502 3 \u2502 user_32 \u2502\n\u2502 3 \u2502 user_52 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show total number of push notifications to iOS devices sent during last 24 hours:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.notifications\nWHERE (time > (now() - toIntervalHour(24))) AND (platform = 'ios')\n\n\u250c\u2500count()\u2500\u2510\n\u2502 31200 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"development",children:"Development"}),"\n",(0,t.jsxs)(n.p,{children:["The recommended way to run ClickHouse in production is with cluster. See ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/tree/master/misc/clickhouse_cluster",children:"an example of such cluster configuration"})," made with Docker Compose."]}),"\n",(0,t.jsx)(n.p,{children:"But during development you may want to run Centrifugo with single instance ClickHouse."}),"\n",(0,t.jsx)(n.p,{children:"To do this set only one ClickHouse dsn and do not set cluster name:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "",\n "export_connections": true,\n "export_subscriptions": true,\n "export_publications": true,\n "export_operations": true,\n "export_http_headers": [\n "Origin",\n "User-Agent"\n ]\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse locally:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm -v /tmp/clickhouse:/var/lib/clickhouse -p 9000:9000 --name click clickhouse/clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse client:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm --link click:clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server --host clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Issue queries:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:":) SELECT * FROM centrifugo.operations\n\n\u250c\u2500client\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500user\u2500\u252c\u2500op\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500channel\u2500\u2500\u2500\u2500\u2500\u252c\u2500method\u2500\u252c\u2500error\u2500\u252c\u2500disconnect\u2500\u252c\u2500duration\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500time\u2500\u2510\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connecting \u2502 \u2502 \u2502 0 \u2502 0 \u2502 217894 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connect \u2502 \u2502 \u2502 0 \u2502 0 \u2502 0 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 92714 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 presence \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 3539 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test1 \u2502 \u2502 0 \u2502 0 \u2502 2402 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test2 \u2502 \u2502 0 \u2502 0 \u2502 634 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test3 \u2502 \u2502 0 \u2502 0 \u2502 412 \u2502 2021-07-31 08:15:12 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"how-export-works",children:"How export works"}),"\n",(0,t.jsx)(n.p,{children:"When ClickHouse analytics enabled Centrifugo nodes start exporting events to ClickHouse. Each node issues insert with events once in 10 seconds (flushing collected events in batches thus making insertion in ClickHouse efficient). Maximum batch size is 100k for each table at the momemt. If insert to ClickHouse failed Centrifugo retries it once and then buffers events in memory (up to 1 million entries). If ClickHouse still unavailable after collecting 1 million events then new events will be dropped until buffer has space. These limits are configurable. Centrifugo PRO uses very efficient code for writing data to ClickHouse, so analytics feature should only add a little overhead for Centrifugo node."}),"\n",(0,t.jsx)(n.p,{children:"Several metrics are exposed to monitor export process health:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_flush_duration_seconds summary"}),"\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_batch_size summary"}),"\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_drop_count counter"}),"\n"]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},14323:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/clickhouse-9b4cfb14c477ffac11caf495a679bd94.png"},37508:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/pro_analytics-b47bb94134e4da79361e6c901f6917f7.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>o});var t=i(67294);const s={},r=t.createContext(s);function o(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9604],{19036:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=i(85893),s=i(11151);const r={id:"analytics",title:"Analytics with ClickHouse"},o=void 0,c={id:"pro/analytics",title:"Analytics with ClickHouse",description:"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.",source:"@site/versioned_docs/version-4/pro/analytics.md",sourceDirName:"pro",slug:"/pro/analytics",permalink:"/docs/4/pro/analytics",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/pro/analytics.md",tags:[],version:"4",frontMatter:{id:"analytics",title:"Analytics with ClickHouse"},sidebar:"Pro",previous:{title:"User and channel tracing",permalink:"/docs/4/pro/tracing"},next:{title:"Operation throttling",permalink:"/docs/4/pro/throttling"}},a={},l=[{value:"Configuration",id:"configuration",level:2},{value:"Connections table",id:"connections-table",level:2},{value:"Subscriptions table",id:"subscriptions-table",level:2},{value:"Operations table",id:"operations-table",level:2},{value:"Publications table",id:"publications-table",level:2},{value:"Notifications table",id:"notifications-table",level:2},{value:"Query examples",id:"query-examples",level:2},{value:"Development",id:"development",level:2},{value:"How export works",id:"how-export-works",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["This feature allows exporting information about channel publications, client connections, channel subscriptions, client operations and push notifications to ",(0,t.jsx)(n.a,{href:"https://clickhouse.com/",children:"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."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"clickhouse",src:i(50895).Z+"",width:"3346",height:"1067"})}),"\n",(0,t.jsx)(n.p,{children:"This unlocks a great observability and a way to perform various analytics queries for better connection behavior understanding, check application correctness, building trends, reports, and so on."}),"\n",(0,t.jsx)(n.p,{children:"As soon as you start using integration with ClickHouse some of mentioned possibilities may be easily accessed with Centrifugo PRO web UI and it's analytics page:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Admin analytics",src:i(4862).Z+"",width:"2854",height:"1396"})}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsx)(n.p,{children:"To enable integration with ClickHouse add the following section to a configuration file:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000",\n "tcp://127.0.0.1:9001",\n "tcp://127.0.0.1:9002",\n "tcp://127.0.0.1:9003"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "centrifugo_cluster",\n "export_connections": true,\n "export_subscriptions": true,\n "export_operations": true,\n "export_publications": true,\n "export_notifications": true,\n "export_http_headers": [\n "User-Agent",\n "Origin",\n "X-Real-Ip"\n ]\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All ClickHouse analytics options scoped to ",(0,t.jsx)(n.code,{children:"clickhouse_analytics"})," section of configuration."]}),"\n",(0,t.jsxs)(n.p,{children:["Toggle this feature using ",(0,t.jsx)(n.code,{children:"enabled"})," boolean option."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["While we have a nested configuration here it's still possible to use environment variables to set options. For example, use ",(0,t.jsx)(n.code,{children:"CENTRIFUGO_CLICKHOUSE_ANALYTICS_ENABLED"})," env var name for configure ",(0,t.jsx)(n.code,{children:"enabled"})," option mentioned above. I.e. nesting expressed as ",(0,t.jsx)(n.code,{children:"_"})," in Centrifugo."]})}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo can export data to different ClickHouse instances, addresses of ClickHouse can be set over ",(0,t.jsx)(n.code,{children:"clickhouse_dsn"})," option."]}),"\n",(0,t.jsxs)(n.p,{children:["You also need to set a ClickHouse cluster name (",(0,t.jsx)(n.code,{children:"clickhouse_cluster"}),") and database name ",(0,t.jsx)(n.code,{children:"clickhouse_database"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_connections"})," tells Centrifugo to export connection information snapshots. Information about connection will be exported once a connection established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_subscriptions"})," tells Centrifugo to export subscription information snapshots. Information about subscription will be exported once a subscription established and then periodically while connection alive. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_operations"})," tells Centrifugo to export individual client operation information. See below on table structure to see which fields are available."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_publications"})," tells Centrifugo to export publications for channels to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_notifications"})," tells Centrifugo to export push notifications to a separate ClickHouse table."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_http_headers"})," is a list of HTTP headers to export for connection information."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"export_grpc_metadata"})," is a list of metadata keys to export for connection information for GRPC unidirectional transport."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_schema_initialization"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". By default Centrifugo tries to initialize table schema on start (if not exists). This flag allows skipping initialization process."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"skip_ping_on_start"})," - boolean, default ",(0,t.jsx)(n.code,{children:"false"}),". Centrifugo pings Clickhouse servers by default on start, if any of servers is unavailable \u2013 Centrifugo fails to start. This option allow skipping this check thus Centrifugo is able to start even if Clickhouse cluster not working correctly."]}),"\n",(0,t.jsx)(n.h2,{id:"connections-table",children:"Connections table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/connections', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.connections_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.connections_distributed\n(\n `client` String,\n `user` String,\n `name` String,\n `version` String,\n `transport` String,\n `headers` Map(String, Array(String)),\n `metadata` Map(String, Array(String)),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'connections', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"subscriptions-table",children:"Subscriptions table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.subscriptions_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.subscriptions_distributed\n(\n `client` String,\n `user` String,\n `channels` Array(String),\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'subscriptions', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"operations-table",children:"Operations table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/operations', '{replica}')\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.operations_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `client` String,\n `user` String,\n `op` String,\n `channel` String,\n `method` String,\n `error` UInt32,\n `disconnect` UInt32,\n `duration` UInt64,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'operations', murmurHash3_64(client)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"publications-table",children:"Publications table"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.publications\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.publications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `channel` String,\n `source` String,\n `size` UInt64,\n `client` String,\n `user` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'publications', murmurHash3_64(channel)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"notifications-table",children:"Notifications table"}),"\n",(0,t.jsxs)(n.p,{children:["\ud83d\udea7 This PRO feature is under construction together with ",(0,t.jsx)(n.a,{href:"/docs/4/pro/push_notifications",children:"push notification API"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.notifications\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMMDD(time)\nORDER BY time\nTTL time + toIntervalDay(1)\nSETTINGS index_granularity = 8192 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"And distributed one:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SHOW CREATE TABLE centrifugo.notifications_distributed;\n\n\u250c\u2500statement\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CREATE TABLE centrifugo.operations_distributed\n(\n `uid` String,\n `provider` String,\n `type` String,\n `recipient` String,\n `device_id` String,\n `platform` String,\n `user` String,\n `msg_id` String,\n `status` String,\n `error_message` String,\n `error_code` String,\n `time` DateTime\n)\nENGINE = Distributed('centrifugo_cluster', 'centrifugo', 'notifications', murmurHash3_64(uid)) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"query-examples",children:"Query examples"}),"\n",(0,t.jsx)(n.p,{children:"Show unique users which were connected:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT DISTINCT user\nFROM centrifugo.connections_distributed;\n\n\u250c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 user_1 \u2502\n\u2502 user_2 \u2502\n\u2502 user_3 \u2502\n\u2502 user_4 \u2502\n\u2502 user_5 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show total number of publication attempts which were throttled by Centrifugo (received ",(0,t.jsx)(n.code,{children:"Too many requests"})," error with code ",(0,t.jsx)(n.code,{children:"111"}),"):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 4502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"The same for a specific user:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.operations_distributed\nWHERE (error = 111) AND (op = 'publish') AND (user = 'user_200');\n\n\u250c\u2500count()\u2500\u2510\n\u2502 1214 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show number of unique users subscribed to a specific channel in last 5 minutes (this is approximate since subscriptions table contain periodic snapshot entries, clients could unsubscribe in between snapshots \u2013 this is reflected in operations table):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(Distinct(user))\nFROM centrifugo.subscriptions_distributed\nWHERE arrayExists(x -> (x = 'chat:index'), channels) AND (time >= (now() - toIntervalMinute(5)));\n\n\u250c\u2500uniqExact(user)\u2500\u2510\n\u2502 101 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Show top 10 users which called ",(0,t.jsx)(n.code,{children:"publish"})," operation during last one minute:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT\n COUNT(op) AS num_ops,\n user\nFROM centrifugo.operations_distributed\nWHERE (op = 'publish') AND (time >= (now() - toIntervalMinute(1)))\nGROUP BY user\nORDER BY num_ops DESC\nLIMIT 10;\n\n\u250c\u2500num_ops\u2500\u252c\u2500user\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 56 \u2502 user_200 \u2502\n\u2502 11 \u2502 user_75 \u2502\n\u2502 6 \u2502 user_87 \u2502\n\u2502 6 \u2502 user_65 \u2502\n\u2502 6 \u2502 user_39 \u2502\n\u2502 5 \u2502 user_28 \u2502\n\u2502 5 \u2502 user_63 \u2502\n\u2502 5 \u2502 user_89 \u2502\n\u2502 3 \u2502 user_32 \u2502\n\u2502 3 \u2502 user_52 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.p,{children:"Show total number of push notifications to iOS devices sent during last 24 hours:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"SELECT COUNT(*)\nFROM centrifugo.notifications\nWHERE (time > (now() - toIntervalHour(24))) AND (platform = 'ios')\n\n\u250c\u2500count()\u2500\u2510\n\u2502 31200 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"development",children:"Development"}),"\n",(0,t.jsxs)(n.p,{children:["The recommended way to run ClickHouse in production is with cluster. See ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/tree/master/misc/clickhouse_cluster",children:"an example of such cluster configuration"})," made with Docker Compose."]}),"\n",(0,t.jsx)(n.p,{children:"But during development you may want to run Centrifugo with single instance ClickHouse."}),"\n",(0,t.jsx)(n.p,{children:"To do this set only one ClickHouse dsn and do not set cluster name:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "clickhouse_analytics": {\n "enabled": true,\n "clickhouse_dsn": [\n "tcp://127.0.0.1:9000"\n ],\n "clickhouse_database": "centrifugo",\n "clickhouse_cluster": "",\n "export_connections": true,\n "export_subscriptions": true,\n "export_publications": true,\n "export_operations": true,\n "export_http_headers": [\n "Origin",\n "User-Agent"\n ]\n }\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse locally:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm -v /tmp/clickhouse:/var/lib/clickhouse -p 9000:9000 --name click clickhouse/clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run ClickHouse client:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"docker run -it --rm --link click:clickhouse-server --entrypoint clickhouse-client clickhouse/clickhouse-server --host clickhouse-server\n"})}),"\n",(0,t.jsx)(n.p,{children:"Issue queries:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:":) SELECT * FROM centrifugo.operations\n\n\u250c\u2500client\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500user\u2500\u252c\u2500op\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500channel\u2500\u2500\u2500\u2500\u2500\u252c\u2500method\u2500\u252c\u2500error\u2500\u252c\u2500disconnect\u2500\u252c\u2500duration\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500time\u2500\u2510\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connecting \u2502 \u2502 \u2502 0 \u2502 0 \u2502 217894 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 connect \u2502 \u2502 \u2502 0 \u2502 0 \u2502 0 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 92714 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 presence \u2502 $chat:index \u2502 \u2502 0 \u2502 0 \u2502 3539 \u2502 2021-07-31 08:15:09 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test1 \u2502 \u2502 0 \u2502 0 \u2502 2402 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test2 \u2502 \u2502 0 \u2502 0 \u2502 634 \u2502 2021-07-31 08:15:12 \u2502\n\u2502 bd55ae3a-dd44-47cb-a4cc-c41f8e33803b \u2502 2694 \u2502 subscribe \u2502 test3 \u2502 \u2502 0 \u2502 0 \u2502 412 \u2502 2021-07-31 08:15:12 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"how-export-works",children:"How export works"}),"\n",(0,t.jsx)(n.p,{children:"When ClickHouse analytics enabled Centrifugo nodes start exporting events to ClickHouse. Each node issues insert with events once in 10 seconds (flushing collected events in batches thus making insertion in ClickHouse efficient). Maximum batch size is 100k for each table at the momemt. If insert to ClickHouse failed Centrifugo retries it once and then buffers events in memory (up to 1 million entries). If ClickHouse still unavailable after collecting 1 million events then new events will be dropped until buffer has space. These limits are configurable. Centrifugo PRO uses very efficient code for writing data to ClickHouse, so analytics feature should only add a little overhead for Centrifugo node."}),"\n",(0,t.jsx)(n.p,{children:"Several metrics are exposed to monitor export process health:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_flush_duration_seconds summary"}),"\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_batch_size summary"}),"\n",(0,t.jsx)(n.li,{children:"centrifugo_clickhouse_analytics_drop_count counter"}),"\n"]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},50895:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/clickhouse-9b4cfb14c477ffac11caf495a679bd94.png"},4862:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/pro_analytics-b47bb94134e4da79361e6c901f6917f7.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>c,a:()=>o});var t=i(67294);const s={},r=t.createContext(s);function o(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4ebb2955.65567078.js b/assets/js/4ebb2955.65567078.js deleted file mode 100644 index 3c9eb0a9e..000000000 --- a/assets/js/4ebb2955.65567078.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3190],{90368:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var t=i(85893),s=i(11151);const o={title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",tags:["centrifugo","redis","go"],description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/redis_cover.png",hide_table_of_contents:!1},r=void 0,a={permalink:"/blog/2022/12/20/improving-redis-engine-performance",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2022-12-20-improving-redis-engine-performance.md",source:"@site/blog/2022-12-20-improving-redis-engine-performance.md",title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",date:"2022-12-20T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"redis",permalink:"/blog/tags/redis"},{label:"go",permalink:"/blog/tags/go"}],readingTime:28.995,hasTruncateMarker:!0,authors:[{name:"Alexander Emelin",title:"Author of Centrifugo",imageURL:"https://github.com/FZambia.png"}],frontMatter:{title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",tags:["centrifugo","redis","go"],description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/redis_cover.png",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket",permalink:"/blog/2023/03/31/keycloak-sso-centrifugo"},nextItem:{title:"101 ways to subscribe user on a personal channel in Centrifugo",permalink:"/blog/2022/07/29/101-way-to-subscribe"}},d={authorsImageUrls:[void 0]},l=[{value:"Broker and PresenceManager",id:"broker-and-presencemanager",level:2},{value:"Redigo",id:"redigo",level:2},{value:"Redigo with pipelining",id:"redigo-with-pipelining",level:2},{value:"Motivation to migrate",id:"motivation-to-migrate",level:2},{value:"Go-redis/redis",id:"go-redisredis",level:2},{value:"Rueidis",id:"rueidis",level:2},{value:"Switching to Rueidis: reducing CPU usage",id:"switching-to-rueidis-reducing-cpu-usage",level:2},{value:"Adding latency",id:"adding-latency",level:2},{value:"Conclusion",id:"conclusion",level:2}];function c(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",h2:"h2",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Centrifugo_Redis_Engine_Improvements",src:i(5443).Z+"",width:"1200",height:"369"})}),"\n",(0,t.jsxs)(n.p,{children:["The main objective of Centrifugo is to manage persistent client connections established over various real-time transports (including WebSocket, HTTP-Streaming, SSE, WebTransport, etc \u2013 see ",(0,t.jsx)(n.a,{href:"https://centrifugal.dev/docs/transports/overview",children:"here"}),") and offer an API for publishing data towards established connections. Clients subscribe to channels, hence Centrifugo implements PUB/SUB mechanics to transmit published data to all online channel subscribers."]}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo employs ",(0,t.jsx)(n.a,{href:"https://redis.com/",children:"Redis"})," as its primary scalability option \u2013 so that it's possible to distribute client connections amongst numerous Centrifugo nodes without worrying about channel subscribers connected to separate nodes. Redis is incredibly mature, simple, and fast in-memory storage. Due to various built-in data structures and PUB/SUB support Redis is a perfect fit to be both Centrifugo ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," (we will describe what's this shortly)."]}),"\n",(0,t.jsxs)(n.p,{children:["In Centrifugo v4.1.0 we introduced an updated implementation of our Redis Engine (",(0,t.jsx)(n.code,{children:"Engine"})," in Centrifugo == ",(0,t.jsx)(n.code,{children:"Broker"})," + ",(0,t.jsx)(n.code,{children:"PresenceManager"}),") which provides sufficient performance improvements to our users. This post discusses the factors that prompted us to update Redis Engine implementation and provides some insight into the results we managed to achieve. We'll examine a few well-known Go libraries for Redis communication and contrast them against Centrifugo tasks."]}),"\n",(0,t.jsx)(n.h2,{id:"broker-and-presencemanager",children:"Broker and PresenceManager"}),"\n",(0,t.jsxs)(n.p,{children:["Before we get started, let's define what Centrifugo's ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," terms mean."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L97",children:"Broker"})," is an interface responsible for maintaining subscriptions from different Centrifugo nodes (initiated by client connections). That helps to scale client connections over many Centrifugo instances and not worry about the same channel subscribers being connected to different nodes \u2013 since all Centrifugo nodes connected with PUB/SUB. Messages published to one node are delivered to a channel subscriber connected to another node."]}),"\n",(0,t.jsxs)(n.p,{children:["Another major part of ",(0,t.jsx)(n.code,{children:"Broker"})," is keeping an expiring publication history for channels (streams). So that Centrifugo may provide a fast cache for messages missed by clients upon going offline for a short period and compensate at most once delivery of Redis PUB/SUB using ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L9",children:"Publication"})," incremental offsets. Centrifugo uses STREAM and HASH data structures in Redis to store channel history and stream meta information."]}),"\n",(0,t.jsxs)(n.p,{children:["In general Centrifugo architecture may be perfectly illustrated by this picture (Gophers are Centrifugo nodes all connected to ",(0,t.jsx)(n.code,{children:"Broker"}),", and sockets are WebSockets):"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:"https://i.imgur.com/QOJ1M9a.png",alt:"gopher-broker"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/presence.go#L12",children:"PresenceManager"})," is an interface responsible for managing online presence information - list of currently active channel subscribers. While the connection is alive we periodically update presence entries for channels connection subscribed to (for channels where presence is enabled). Presence data should expire if not updated by a client connection for some time. Centrifugo uses two Redis data structures for managing presence in channels - HASH and ZSET."]}),"\n",(0,t.jsx)(n.h2,{id:"redigo",children:"Redigo"}),"\n",(0,t.jsxs)(n.p,{children:["For a long time, the ",(0,t.jsx)(n.a,{href:"https://github.com/gomodule/redigo",children:"gomodule/redigo"})," package served as the foundation for the Redis Engine implementation in Centrifugo. Huge props go to ",(0,t.jsx)(n.a,{href:"https://github.com/garyburd",children:"Mr Gary Burd"})," for creating it."]}),"\n",(0,t.jsxs)(n.p,{children:["Redigo offers a connection ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/gomodule/redigo/redis#Pool",children:"Pool"})," to Redis. A simple usage of it involves getting the connection from the pool, issuing request to Redis over that connection, and then putting the connection back to the pool after receiving the result from Redis."]}),"\n",(0,t.jsx)(n.p,{children:"Let's write a simple benchmark which demonstrates simple usage of Redigo and measures SET operation performance:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkRedigo(b *testing.B) {\n\tpool := redigo.Pool{\n\t\tMaxIdle: 128,\n\t\tMaxActive: 128,\n\t\tWait: true,\n\t\tDial: func() (redigo.Conn, error) {\n\t\t\treturn redigo.Dial("tcp", ":6379")\n\t\t},\n\t}\n\tdefer pool.Close()\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tc := pool.Get()\n\t\t\t_, err := c.Do("SET", "redigo", "test")\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t\tc.Close()\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Let's run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Seems pretty fast, but we can improve it further."}),"\n",(0,t.jsx)(n.h2,{id:"redigo-with-pipelining",children:"Redigo with pipelining"}),"\n",(0,t.jsxs)(n.p,{children:["To increase a throughput in Centrifugo, instead of using Redigo's ",(0,t.jsx)(n.code,{children:"Pool"})," for each operation, we acquired a dedicated connection from the ",(0,t.jsx)(n.code,{children:"Pool"})," and utilized ",(0,t.jsx)(n.a,{href:"https://redis.io/docs/manual/pipelining/",children:"Redis pipelining"})," to send multiple commands where possible."]}),"\n",(0,t.jsx)(n.p,{children:"Redis pipelining improves performance by executing multiple commands using a single client-server-client round trip. Instead of executing many commands one by one, you can queue the commands in a pipeline and then execute the queued commands as if it is a single command. Redis processes commands in order and sends individual response for each command. Given a single CPU nature of Redis, reducing the number of active connections when using pipelining has a positive impact on throughput \u2013 therefore pipelining is beneficial from this angle as well."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Redis pipeline",src:i(79696).Z+"",width:"2556",height:"1564"})}),"\n",(0,t.jsxs)(n.p,{children:["You can quickly estimate the benefits of pipelining by running Redis locally and running ",(0,t.jsx)(n.code,{children:"redis-benchmark"})," which comes with Redis distribution over it:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"> redis-benchmark -n 100000 set key value\n\nSummary:\n throughput summary: 84674.01 requests per second\n"})}),"\n",(0,t.jsx)(n.p,{children:"And with pipelining:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"> redis-benchmark -n 100000 -P 64 set key value\n\nSummary:\n throughput summary: 666880.00 requests per second\n"})}),"\n",(0,t.jsxs)(n.p,{children:["In Centrifugo we are using smart batching technique for collecting pipeline (also described in ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket",children:"one of the previous posts"})," in this blog)."]}),"\n",(0,t.jsx)(n.p,{children:"To demonstrate benefits from using pipelining let's look at the following benchmark:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'const (\n\tmaxCommandsInPipeline = 512\n\tnumPipelineWorkers = 1\n)\n\ntype command struct {\n\terrCh chan error\n}\n\ntype sender struct {\n\tcmdCh chan command\n\tpool redigo.Pool\n}\n\nfunc newSender(pool redigo.Pool) *sender {\n\tp := &sender{\n\t\tcmdCh: make(chan command),\n\t\tpool: pool,\n\t}\n\tgo func() {\n\t\tfor {\n\t\t\tfor i := 0; i < numPipelineWorkers; i++ {\n\t\t\t\tp.runPipelineRoutine()\n\t\t\t}\n\t\t}\n\t}()\n\treturn p\n}\n\nfunc (s *sender) send() error {\n\terrCh := make(chan error, 1)\n\tcmd := command{\n\t\terrCh: errCh,\n\t}\n\t// Submit command to be executed by runPipelineRoutine.\n\ts.cmdCh <- cmd\n\treturn <-errCh\n}\n\nfunc (s *sender) runPipelineRoutine() {\n\tconn := p.pool.Get()\n\tdefer conn.Close()\n\tfor {\n\t\tselect {\n\t\tcase cmd := <-s.cmdCh:\n\t\t\tcommands := []command{cmd}\n\t\t\tconn.Send("set", "redigo", "test")\n\t\tloop:\n\t\t\t// Collect batch of commands to send to Redis in one RTT.\n\t\t\tfor i := 0; i < maxCommandsInPipeline; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase cmd := <-s.cmdCh:\n\t\t\t\t\tcommands = append(commands, cmd)\n\t\t\t\t\tconn.Send("set", "redigo", "test")\n\t\t\t\tdefault:\n\t\t\t\t\tbreak loop\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush all collected commands to the network.\n\t\t\terr := conn.Flush()\n\t\t\tif err != nil {\n\t\t\t\tfor i := 0; i < len(commands); i++ {\n\t\t\t\t\tcommands[i].errCh <- err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Read responses to commands, they come in order.\n\t\t\tfor i := 0; i < len(commands); i++ {\n\t\t\t\t_, err := conn.Receive()\n\t\t\t\tcommands[i].errCh <- err\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc BenchmarkRedigoPipelininig(b *testing.B) {\n\tpool := redigo.Pool{\n\t\tWait: true,\n\t\tDial: func() (redigo.Conn, error) {\n\t\t\treturn redigo.Dial("tcp", ":6379")\n\t\t},\n\t}\n\tdefer pool.Close()\n\n\tsender := newSender(pool)\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\terr := sender.send()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"This is a strategy that we employed in Centrifugo for a long time. As you can see code with automatic pipelining gets more complex, and in real life it's even more complicated to support different types of commands, channel send timeouts, and server shutdowns."}),"\n",(0,t.jsx)(n.p,{children:"What about the performance of this approach?"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkRedigoPipelininig-8 1840758 604.7 ns/op 176 B/op 4 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Operation latency reduced from 4648 ns/op to 604.7 ns/op \u2013 not bad right?"}),"\n",(0,t.jsxs)(n.p,{children:["It's worth mentioning that upon increased RTT between application and Redis the approach with pipelining will provide worse throughput. But it still can be better than in pool-based approach. Let's say we have latency 5ms between app and Redis. This means that with pool size of 128 you will be able to issue up to ",(0,t.jsx)(n.code,{children:"128 * (1000 / 5) = 25600"})," requests per second over 128 connections. With the pipelining approach above the theoretical limit is ",(0,t.jsx)(n.code,{children:"512 * (1000 / 5) = 102400"})," requests per second over a single connection (though in case of using code for pipelining shown above we need to have larger parallelism, say 512 instead of 128). And it can scale further if you increase ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," to work over several connections in paralell. Though increasing ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," has negative effect on CPU \u2013 we will discuss this later in this post."]}),"\n",(0,t.jsx)(n.p,{children:"Redigo is an awesome battle-tested library that served us great for a long time."}),"\n",(0,t.jsx)(n.h2,{id:"motivation-to-migrate",children:"Motivation to migrate"}),"\n",(0,t.jsx)(n.p,{children:"There are three modes in which Centrifugo can work with Redis these days:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Connecting to a standalone single Redis instance"}),"\n",(0,t.jsx)(n.li,{children:"Connecting to Redis in master-replica configuration, where Redis Sentinel controls the failover process"}),"\n",(0,t.jsx)(n.li,{children:"Connecting to Redis Cluster"}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"All modes additionally can be used with client-side consistent sharding. So it's possible to scale Redis even without a Redis Cluster setup."}),"\n",(0,t.jsx)(n.p,{children:"Unfortunately, with pure Redigo library, it's only possible to implement [ 1 ] \u2013 i.e. connecting to a single standalone Redis instance."}),"\n",(0,t.jsx)(n.p,{children:"To support the scheme with Sentinel you whether need to have a proxy between the application and Redis which proxies the connection to Redis master. For example, with Haproxy it's possible in this way:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"listen redis\n server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions\n server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup\n bind *:6379\n mode tcp\n option tcpka\n option tcplog\n option tcp-check\n tcp-check send PING\\r\\n\n tcp-check expect string +PONG\n tcp-check send info\\ replication\\r\\n\n tcp-check expect string role:master\n tcp-check send QUIT\\r\\n\n tcp-check expect string +OK\n balance roundrobin\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Or, you need to additionally import ",(0,t.jsx)(n.a,{href:"https://github.com/FZambia/sentinel",children:"FZambia/sentinel"})," library - which provides a communication layer with Redis Sentinel on top of Redigo's connection Pool."]}),"\n",(0,t.jsxs)(n.p,{children:["For communicating with Redis Cluster one more library may be used \u2013 ",(0,t.jsx)(n.a,{href:"https://github.com/mna/redisc",children:"mna/redisc"})," which is also a layer on top of ",(0,t.jsx)(n.code,{children:"redigo"})," basic functionality."]}),"\n",(0,t.jsxs)(n.p,{children:["Combining ",(0,t.jsx)(n.code,{children:"redigo"})," + ",(0,t.jsx)(n.code,{children:"FZambia/sentinel"})," + ",(0,t.jsx)(n.code,{children:"mna/redisc"})," we managed to implement all three connection modes. This worked, though resulted in rather tricky Redis setup. Also, it was difficult to re-use existing pipelining code we had for a standalone Redis with Redis Cluster. As a result, Centrifugo only used pipelining in a standalone or Sentinel Redis cases. When using Redis Cluster, however, Centrifugo merely used the connection pool to issue requests thus not benefiting from request pipelining. Due to this we had some code duplication to send the same requests in various Redis configurations."]}),"\n",(0,t.jsxs)(n.p,{children:["Another thing is that Redigo uses ",(0,t.jsx)(n.code,{children:"interface{}"})," for command construction. To send command to Redis Redigo has ",(0,t.jsx)(n.code,{children:"Do"})," method which accepts name of the command and variadic ",(0,t.jsx)(n.code,{children:"interface{}"})," arguments to construct command arguments:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"Do(commandName string, args ...interface{}) (reply interface{}, err error)\n"})}),"\n",(0,t.jsx)(n.p,{children:"While this works well and you can issue any command to Redis, you need to be very accurate when constructing a command. This also adds some allocation overhead. As we know more memory allocations lead to the increased CPU utilization because the allocation process itself requires more processing power and the GC is under more strain."}),"\n",(0,t.jsxs)(n.p,{children:["At some point we felt that eliminating additional dependencies (even though I am the author of one of them) and reducing allocations in Redis communication layer is a nice step forward for Centrifugo. So we started looking around for ",(0,t.jsx)(n.code,{children:"redigo"})," alternatives."]}),"\n",(0,t.jsx)(n.p,{children:"To summarize, here is what we wanted from Redis library:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Possibility to work with all three Redis setup options we support: standalone, master-replica(s) with Sentinel, Redis Cluster, so we can depend on one library instead of three"}),"\n",(0,t.jsx)(n.li,{children:"Less memory allocations (and more type-safety API is a plus)"}),"\n",(0,t.jsx)(n.li,{children:"Support working with RESP2-only Redis servers as we need that for backwards compatibility. And some vendors like Redis Enterprise still support RESP2 protocol only"}),"\n",(0,t.jsx)(n.li,{children:"The library should be actively maintained"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"go-redisredis",children:"Go-redis/redis"}),"\n",(0,t.jsxs)(n.p,{children:["The most obvious alternative to Redigo is ",(0,t.jsx)(n.a,{href:"https://github.com/go-redis/redis",children:"go-redis/redis"})," package. It's popular, regularly gets updates, used by a huge amount of Go projects (Grafana, Thanos, etc.). And maintained by\n",(0,t.jsx)(n.a,{href:"https://github.com/vmihailenco",children:"Vladimir Mihailenco"})," who created several more awesome Go libraries, like ",(0,t.jsx)(n.a,{href:"https://github.com/vmihailenco/msgpack",children:"msgpack"})," for example. I personally successfully used ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in several other projects I worked on."]}),"\n",(0,t.jsxs)(n.p,{children:["To avoid setup boilerplate for various Redis installation variations ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," has ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#UniversalClient",children:"UniversalClient"}),". From docs:"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"UniversalClient is a wrapper client which, based on the provided options, represents either a ClusterClient, a FailoverClient, or a single-node Client. This can be useful for testing cluster-specific applications locally or having different clients in different environments."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In terms of implementation ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," also has internal pool of connections to Redis, similar to ",(0,t.jsx)(n.code,{children:"redigo"}),". It's also possible to use ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Pipeline",children:"Client.Pipeline"})," method to allocate a ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Pipeliner",children:"Pipeliner"})," interface and use it for pipelining. So ",(0,t.jsx)(n.code,{children:"UniversalClient"})," reduces setup boilerplate for different Redis installation types and number of dependencies we had, and it provide very similar way to pipeline requests so we could easily re-implement things we had with Redigo."]}),"\n",(0,t.jsxs)(n.p,{children:["Go-redis also provides more type-safety when constructing commands compared to Redigo, almost every command in Redis is implemented as a separate method of ",(0,t.jsx)(n.code,{children:"Client"}),", for example ",(0,t.jsx)(n.code,{children:"Publish"})," ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Publish",children:"defined"})," as:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"func (c Client) Publish(ctx context.Context, channel string, message interface{}) *IntCmd\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can see though that we still have ",(0,t.jsx)(n.code,{children:"interface{}"})," here for ",(0,t.jsx)(n.code,{children:"message"})," argument type. I suppose this was implemented in such way for convenience \u2013 to pass both ",(0,t.jsx)(n.code,{children:"string"})," or ",(0,t.jsx)(n.code,{children:"[]byte"}),". But it still produces some extra allocations."]}),"\n",(0,t.jsxs)(n.p,{children:["Without pipelining the simplest program with ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," may look like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkGoredis(b *testing.B) {\n\tclient := redis.NewUniversalClient(&redis.UniversalOptions{\n\t\tAddrs: []string{":6379"},\n\t\tPoolSize: 128,\n\t})\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tresp := client.Set(context.Background(), "goredis", "test", 0)\n\t\t\tif resp.Err() != nil {\n\t\t\t\tb.Fatal(resp.Err())\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Let's run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkGoredis-8 268444 4561 ns/op 244 B/op 8 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Result is pretty comparable to Redigo, though Go-redis allocates more (btw most of allocations come from the connection liveness check upon getting from the pool which can not be turned off)."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(44755).Z+"",width:"1884",height:"986"})}),"\n",(0,t.jsxs)(n.p,{children:["It's interesting \u2013 if we dive deeper into what is it we can discover that this is the only way in Go to check connection was closed without reading data from it. The approach was originally introduced ",(0,t.jsx)(n.a,{href:"https://github.com/go-sql-driver/mysql/blob/41dd159e6ec9afad00d2b90144bbc083ea860db1/conncheck.go#L23",children:"by go-sql-driver/mysql"}),", it's not cross-platform, and ",(0,t.jsx)(n.a,{href:"https://github.com/golang/go/issues/15735",children:"related issue"})," may be found in Go issue tracker."]}),"\n",(0,t.jsxs)(n.p,{children:["But as I said in Centrifugo we already used pipelining over the dedicated connection for all operations so we avoid frequently getting connections from the pool. And early experiments proved that ",(0,t.jsx)(n.code,{children:"go-redis"})," may provide some performance benefits for our use case."]}),"\n",(0,t.jsxs)(n.p,{children:["At some point ",(0,t.jsx)(n.a,{href:"https://github.com/j178",children:"@j178"})," sent ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/pull/235",children:"a pull request"})," to Centrifuge library with ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," implementations based on ",(0,t.jsx)(n.code,{children:"go-redis/redis"}),". The amount of code to cover all the various Redis setups was reduced, we got only one dependency instead of three \ud83d\udd25"]}),"\n",(0,t.jsx)(n.p,{children:"But what about performance? Here we will show results for several operations which are typical for Centrifugo:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Publish a message to a channel without saving it to the history - this is just a Redis PUBLISH command going through Redis PUB/SUB system (",(0,t.jsx)(n.code,{children:"RedisPublish"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Publish message to a channel with saving it to history - this involves executing the LUA script on Redis side where we add a publication to STREAM data structure, update meta information HASH, and finally PUBLISH to PUB/SUB (",(0,t.jsx)(n.code,{children:"RedisPublish_History"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Subscribe to a channel - that's a SUBSCRIBE Redis command, this is important to have it fast as Centrifugo should be able to re-subscribe to all the channels in the system upon ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"mass client reconnect scenario"})," (",(0,t.jsx)(n.code,{children:"RedisSubscribe"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Recovering missed publication state from channel STREAM, this is again may be called lots of times when all clients reconnect at once (",(0,t.jsx)(n.code,{children:"RedisRecover"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Updating connection presence information - many connections may periodically update their channel online presence information in Redis (",(0,t.jsx)(n.code,{children:"RedisAddPresence"}),")"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Here are the benchmark results we got when comparing ",(0,t.jsx)(n.code,{children:"redigo"})," (v1.8.9) implementation (old) and ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," (v9.0.0-rc.2) implementation (new) with Redis v6.2.7 on Mac with M1 processor and benchmark paralellism 128:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"\u276f benchstat redigo_p128.txt goredis_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 1.45\xb5s \xb110% 1.88\xb5s \xb1 4% +29.32% (p=0.000 n=10+10)\nRedisPublish_History-8 12.5\xb5s \xb1 6% 9.7\xb5s \xb1 3% -22.77% (p=0.000 n=10+10)\nRedisSubscribe-8 1.47\xb5s \xb124% 1.47\xb5s \xb110% ~ (p=0.469 n=10+10)\nRedisRecover-8 18.4\xb5s \xb1 2% 6.3\xb5s \xb1 0% -65.78% (p=0.000 n=10+8)\nRedisAddPresence-8 3.72\xb5s \xb1 1% 3.40\xb5s \xb1 1% -8.74% (p=0.000 n=10+10)\n\nname old alloc/op new alloc/op delta\nRedisPublish-8 483B \xb1 0% 499B \xb1 0% +3.37% (p=0.000 n=9+10)\nRedisPublish_History-8 1.30kB \xb1 0% 1.08kB \xb1 0% -16.67% (p=0.000 n=10+10)\nRedisSubscribe-8 892B \xb1 2% 662B \xb1 6% -25.83% (p=0.000 n=10+10)\nRedisRecover-8 1.25kB \xb1 1% 1.00kB \xb1 0% -19.91% (p=0.000 n=10+10)\nRedisAddPresence-8 907B \xb1 0% 827B \xb1 0% -8.82% (p=0.002 n=7+8)\n\nname old allocs/op new allocs/op delta\nRedisPublish-8 10.0 \xb1 0% 9.0 \xb1 0% -10.00% (p=0.000 n=10+10)\nRedisPublish_History-8 29.0 \xb1 0% 25.0 \xb1 0% -13.79% (p=0.000 n=10+10)\nRedisSubscribe-8 22.0 \xb1 0% 14.0 \xb1 0% -36.36% (p=0.000 n=8+7)\nRedisRecover-8 29.0 \xb1 0% 23.0 \xb1 0% -20.69% (p=0.000 n=10+10)\nRedisAddPresence-8 18.0 \xb1 0% 17.0 \xb1 0% -5.56% (p=0.000 n=10+10)\n"})}),"\n",(0,t.jsx)(n.admonition,{type:"danger",children:(0,t.jsx)(n.p,{children:"Please note that this benchmark is not a pure performance comparison of two Go libraries for Redis \u2013 it's a performance comparison of Centrifugo Engine methods upon switching to a new library."})}),"\n",(0,t.jsx)(n.p,{children:"Or visualized in Grafana:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(13539).Z+"",width:"2268",height:"1336"})}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:["Centrifugo benchmarks results shown in the post use parallelism 128. If someone interested to check numbers for paralellism 1 or 16 \u2013 ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugal.dev/pull/18#issuecomment-1356263272",children:"check out this comment on Github"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"We observe a noticeable reduction in allocations in these benchmarks and in most benchmarks (presented here and other not listed in this post) we observed a reduced latency."}),"\n",(0,t.jsxs)(n.p,{children:["Overall, results convinced us that the migration from ",(0,t.jsx)(n.code,{children:"redigo"})," to ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," may provide Centrifugo with everything we aimed for \u2013 all the goals for a ",(0,t.jsx)(n.code,{children:"redigo"})," alternative outlined above were successfully fullfilled."]}),"\n",(0,t.jsxs)(n.p,{children:["One good thing ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," allowed us to do is to use Redis pipelining also in a Redis Cluster case. It's possible due to the fact that ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," ",(0,t.jsx)(n.a,{href:"https://github.com/go-redis/redis/blob/c561f3ca7e5cf44ce1f1d3ef30f4a10a9c674c8a/cluster.go#L1062",children:"re-maps pipeline objects internally"})," based on keys to execute pipeline on the correct node of Redis Cluster. Actually, we could do the same based on ",(0,t.jsx)(n.code,{children:"redigo"})," + ",(0,t.jsx)(n.code,{children:"mna/redisc"}),", but here we got it for free."]}),"\n",(0,t.jsxs)(n.p,{children:["BTW, there is ",(0,t.jsx)(n.a,{href:"https://redis.uptrace.dev/guide/go-redis-vs-redigo.html",children:"a page with comparison"})," between ",(0,t.jsx)(n.code,{children:"redigo"})," and ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," docs which outlines some things I mentioned here and some others."]}),"\n",(0,t.jsxs)(n.p,{children:["But we have not migrated to ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in the end. And the reason is another library \u2013 ",(0,t.jsx)(n.code,{children:"rueidis"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"rueidis",children:"Rueidis"}),"\n",(0,t.jsxs)(n.p,{children:["While results were good with ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," we also made an attempt to implement Redis Engine on top of ",(0,t.jsx)(n.a,{href:"https://github.com/rueian/rueidis",children:"rueian/rueidis"})," library written by ",(0,t.jsx)(n.a,{href:"https://github.com/rueian",children:"@rueian"}),". According to docs, ",(0,t.jsx)(n.code,{children:"rueidis"})," is:"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"A fast Golang Redis client that supports Client Side Caching, Auto Pipelining, Generics OM, RedisJSON, RedisBloom, RediSearch, RedisAI, RedisGears, etc."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The readme of ",(0,t.jsx)(n.code,{children:"rueidis"})," contains benchmark results where it hugely outperforms ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in terms of operation latency/throughput in both single Redis and Redis Custer setups:"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(91177).Z+"",width:"2846",height:"1448"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(23522).Z+"",width:"2846",height:"1438"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"rueidis"})," works with standalone Redis, Sentinel Redis and Redis Cluster out of the box. Just like ",(0,t.jsx)(n.code,{children:"UniversalClient"})," of ",(0,t.jsx)(n.code,{children:"go-redis/redis"}),". So it also allowed us to reduce code boilerplate to work with all these setups."]}),"\n",(0,t.jsx)(n.p,{children:"Again, let's try to write a simple program like we had for Redigo and Go-redis above:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkRueidis(b *testing.B) {\n\tclient, err := rueidis.NewClient(rueidis.ClientOption{\n\t\tInitAddress: []string{":6379"},\n\t})\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tcmd := client.B().Set().Key("rueidis").Value("test").Build()\n\t\t\tres := client.Do(context.Background(), cmd)\n\t\t\tif res.Error() != nil {\n\t\t\t\tb.Fatal(res.Error())\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"And run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkGoredis-8 268444 4561 ns/op 244 B/op 8 allocs/op\nBenchmarkRueidis-8 2908591 418.5 ns/op 4 B/op 1 allocs/op\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"rueidis"})," library comes with ",(0,t.jsx)(n.strong,{children:"automatic implicit pipelining"}),", so you can send each request in isolated way while ",(0,t.jsx)(n.code,{children:"rueidis"})," makes sure the request becomes part of the pipeline sent to Redis \u2013 thus utilizing the connection between an application and Redis most efficiently with maximized throughput. The idea of implicit pipelining with Redis is not new and Go ecosystem already had ",(0,t.jsx)(n.a,{href:"https://github.com/joomcode/redispipe",children:"joomcode/redispipe"})," library which implemented it (though it comes with some limitations which made it unsuitable for Centrifugo use case)."]}),"\n",(0,t.jsxs)(n.p,{children:["So ",(0,t.jsx)(n.strong,{children:"applications that use a pool-based approach"})," for communication with Redis may observe dramatic improvements in latency and throughput when switching to the Rueidis library."]}),"\n",(0,t.jsxs)(n.p,{children:["For Centrifugo we didn't expect such a huge speed-up as shown in the above graphs since we already used pipelining in Redis Engine. But ",(0,t.jsx)(n.code,{children:"rueidis"}),' implements some ideas which allow it to be efficient. Insights about these ideas are provided by Rueidis author in a "Writing a High-Performance Golang Client Library" series of posts on Medium:']}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/writing-high-performance-golang-client-library-part-1-batching-on-pipeline-97988fe3211",children:"Part 1: Batching on Pipeline"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/working-on-high-performance-golang-client-library-reading-again-from-channels-5e98ff3538cf",children:"Part 2: Reading Again From Channels?"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/working-on-high-performance-golang-client-library-remove-the-bad-busy-loops-with-the-sync-cond-e262b3fcb458",children:"Part 3: Remove the Bad Busy Loops With the Sync.Cond"})}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["I did some prototypes with ",(0,t.jsx)(n.code,{children:"rueidis"})," which were super-promising in terms of performance. There were some issues found during that early prototyping (mostly with PUB/SUB) \u2013 but all of them were quickly resolved by Rueian."]}),"\n",(0,t.jsxs)(n.p,{children:["Until ",(0,t.jsx)(n.code,{children:"v0.0.80"})," release ",(0,t.jsx)(n.code,{children:"rueidis"})," did not support RESP2 though, so we could not replace our Redis Engine implementation with it. But as soon as it got RESP2 support we opened ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/pull/262",children:"a pull request with alternative implementation"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Since auto-pipelining is used in ",(0,t.jsx)(n.code,{children:"rueidis"})," by default we were able to remove some of our own pipelining management code \u2013 so the Engine implementation is more concise now. One more thing to mention is a simpler PUB/SUB code we were able to write with ",(0,t.jsx)(n.code,{children:"rueidis"}),". One example is that in ",(0,t.jsx)(n.code,{children:"redigo"})," case we had to periodically PING PUB/SUB connection to maintain it alive, ",(0,t.jsx)(n.code,{children:"rueidis"})," does this automatically."]}),"\n",(0,t.jsxs)(n.p,{children:["Regarding performance, here are the benchmark results we got when comparing ",(0,t.jsx)(n.code,{children:"redigo"})," (v1.8.9) implementation (old) and ",(0,t.jsx)(n.code,{children:"rueidis"})," (v0.0.90) implementation (new):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"\u276f benchstat redigo_p128.txt rueidis_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 1.45\xb5s \xb110% 0.56\xb5s \xb1 1% -61.53% (p=0.000 n=10+9)\nRedisPublish_History-8 12.5\xb5s \xb1 6% 9.7\xb5s \xb1 1% -22.43% (p=0.000 n=10+9)\nRedisSubscribe-8 1.47\xb5s \xb124% 1.45\xb5s \xb1 1% ~ (p=0.484 n=10+9)\nRedisRecover-8 18.4\xb5s \xb1 2% 6.2\xb5s \xb1 1% -66.08% (p=0.000 n=10+10)\nRedisAddPresence-8 3.72\xb5s \xb1 1% 3.60\xb5s \xb1 1% -3.34% (p=0.000 n=10+10)\n\nname old alloc/op new alloc/op delta\nRedisPublish-8 483B \xb1 0% 91B \xb1 0% -81.16% (p=0.000 n=9+10)\nRedisPublish_History-8 1.30kB \xb1 0% 0.39kB \xb1 0% -70.08% (p=0.000 n=10+8)\nRedisSubscribe-8 892B \xb1 2% 360B \xb1 0% -59.66% (p=0.000 n=10+10)\nRedisRecover-8 1.25kB \xb1 1% 0.36kB \xb1 1% -71.52% (p=0.000 n=10+10)\nRedisAddPresence-8 907B \xb1 0% 151B \xb1 1% -83.34% (p=0.000 n=7+9)\n\nname old allocs/op new allocs/op delta\nRedisPublish-8 10.0 \xb1 0% 2.0 \xb1 0% -80.00% (p=0.000 n=10+10)\nRedisPublish_History-8 29.0 \xb1 0% 10.0 \xb1 0% -65.52% (p=0.000 n=10+10)\nRedisSubscribe-8 22.0 \xb1 0% 6.0 \xb1 0% -72.73% (p=0.002 n=8+10)\nRedisRecover-8 29.0 \xb1 0% 7.0 \xb1 0% -75.86% (p=0.000 n=10+10)\nRedisAddPresence-8 18.0 \xb1 0% 3.0 \xb1 0% -83.33% (p=0.000 n=10+10)\n"})}),"\n",(0,t.jsx)(n.p,{children:"Or visualized in Grafana:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(99442).Z+"",width:"2278",height:"1326"})}),"\n",(0,t.jsx)(n.p,{children:"2.5x times more publication throughput than we had before! Instead of 700k publications/sec, we went towards 1.7 million publications/sec due to drastically decreased publish operation latency (1.45\xb5s -> 0.59\xb5s). This means that our previous Engine implementation under-utilized Redis, and Rueidis just pushes us towards Redis limits. The latency of most other operations is also reduced."}),"\n",(0,t.jsxs)(n.p,{children:['The allocation effectiveness of the implementation based on "rueidis" is best. As you can see ',(0,t.jsx)(n.code,{children:"rueidis"})," helped us to generate sufficiently fewer memory allocations for all our Redis operations. Allocation improvements directly affect Centrifugo node CPU usage. Though we will talk about CPU more later below."]}),"\n",(0,t.jsx)(n.p,{children:"For Redis Cluster case we also got benchmark results similar to the standalone Redis results above."}),"\n",(0,t.jsxs)(n.p,{children:["I might add that I enjoyed building commands with ",(0,t.jsx)(n.code,{children:"rueidis"}),". All Redis commands may be constructed using a builder approach. Rueidis comes with builders generated for all Redis commands. As an illustration, this is a process of building a PUBLISH Redis command:"]}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/rueidis_cmd.mp4"}),"\n",(0,t.jsx)(n.p,{children:"This drastically reduces a chance to make a stupid mistake while constructing a command. Instead of always opening Redis docs to see a command syntax it's now possible to just start typing - and quickly come to the complete command to send."}),"\n",(0,t.jsx)(n.h2,{id:"switching-to-rueidis-reducing-cpu-usage",children:"Switching to Rueidis: reducing CPU usage"}),"\n",(0,t.jsx)(n.p,{children:"After making all these benchmarks and implementing Engine in Rueidis I decided to check whether Centrifugo consumes less CPU with it. I expected a notable CPU reduction as Rueidis Engine implementation allocates much less than Redigo-based. Turned out it's not that simple."}),"\n",(0,t.jsx)(n.p,{children:"I ran Centrifugo with some artificial load and noticed that CPU consumption of the new implementation is actually... worse than we had with Redigo-based engine under equal conditions!\ud83d\ude29 But why?"}),"\n",(0,t.jsxs)(n.p,{children:["As I mentioned above Redis pipelining is a technique when several commands may be combined into one batch to send over the network. In case of automatic pipelining the size of generated batches start playing a crucial role in application and Redis CPU usage \u2013 since smaller command batches result into more read/write system calls to the kernel on both application and Redis server sides. That's why projects like ",(0,t.jsx)(n.a,{href:"https://github.com/twitter/twemproxy",children:"Twemproxy"})," which sit between app and Redis have sich a good effect on Redis CPU usage among other things."]}),"\n",(0,t.jsx)(n.p,{children:"As we have seen above, Rueidis provides a better throughput and latency, but it's more agressive in terms of flushing data to the network. So in its default configuration we get smaller batches under th equal conditions than we had before in our own pipelining implementation based on Redigo (shown in the beginning of this post)."}),"\n",(0,t.jsxs)(n.p,{children:["Luckily, there is an option in Rueidis called ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," which allows to slow down write loop a bit to give Rueidis a chance to collect more commands to send in one batch. When this option is used Rueidis will make a pause after each network flush not bigger than selected value of ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," (please note, that this is a delay after flushing collected pipeline commands, not an additional delay for each request). Using some reasonable value it's possible to drastically reduce both application and Redis CPU utilization."]}),"\n",(0,t.jsxs)(n.p,{children:["To demonstrate this I created a repo: ",(0,t.jsx)(n.a,{href:"https://github.com/FZambia/pipelines",children:"https://github.com/FZambia/pipelines"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["This repo contains three benchmarks where we use automatic pipelining: based on ",(0,t.jsx)(n.code,{children:"redigo"}),", based on ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," and ",(0,t.jsx)(n.code,{children:"rueidis"}),". In these benchmarks we produce concurrent requests, but instead of pushing the system towards the limits we are limiting number of requests sent to Redis, so we put all libraries in equal conditions."]}),"\n",(0,t.jsxs)(n.p,{children:["To rate limit requests we are using ",(0,t.jsx)(n.a,{href:"https://github.com/uber-go/ratelimit",children:"uber-go/ratelimit"})," library. For example, to allow rate no more than 100k commands per second we can do sth like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"rl := ratelimit.New(100, ratelimit.Per(time.Millisecond))\nfor {\n\trl.Take()\n\t...\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["We limit requests per second we could actually just write ",(0,t.jsx)(n.code,{children:"ratelimit.New(100000)"})," \u2013 but we aim to get a more smooth distribution of requests over time - so using millisecond resolution."]}),"\n",(0,t.jsx)(n.p,{children:"Let's run all the benchmarks in the default configuration:"}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/redis_b1.mp4"}),"\n",(0,t.jsx)(n.p,{children:"Average CPU usage during the test (a bit rough but enough for demonstration):"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{}),(0,t.jsx)(n.th,{children:"Redigo"}),(0,t.jsx)(n.th,{children:"Go-redis/redis"}),(0,t.jsx)(n.th,{children:"Rueidis"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Application CPU, %"}),(0,t.jsx)(n.td,{children:"95"}),(0,t.jsx)(n.td,{children:"99"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"red"},children:"116"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Redis CPU, %"}),(0,t.jsx)(n.td,{children:"36"}),(0,t.jsx)(n.td,{children:"35"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"red"},children:"42"})})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["OK, Rueidis-based implementation is the worst here despite of allocating less than others. So let's try to change this by setting ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," to sth like 100 microseconds:"]}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/redis_b2.mp4"}),"\n",(0,t.jsx)(n.p,{children:"Now CPU usage is:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{}),(0,t.jsx)(n.th,{children:"Redigo"}),(0,t.jsx)(n.th,{children:"Go-redis/redis"}),(0,t.jsx)(n.th,{children:"Rueidis"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Application CPU, %"}),(0,t.jsx)(n.td,{children:"95"}),(0,t.jsx)(n.td,{children:"99"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"green"},children:"59"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Redis CPU, %"}),(0,t.jsx)(n.td,{children:"36"}),(0,t.jsx)(n.td,{children:"35"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"green"},children:"12"})})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"So we can achieve great CPU usage reduction. CPU went from 116% to 59% for the application side, and from 42% to only 12% for Redis! We are sacrificing latency though. Given the fact the CPU utilization reduction is very notable the trade-off is pretty fair."}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsx)(n.p,{children:"It's definitely possible to improve CPU usage in Redigo and Go-redis/redis cases too \u2013 using similar technique. But the goal here was to improve Rueidis-based engine implementation to make it comparable or better than our Redigo-based implementation in terms of CPU utilization."})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see we were able to achieve better CPU results just by using 100 microseconds delay after each network flush. In real life, where we are not running Redis on localhost and have some network latency in between application and Redis, this delay should be insignificant at all. Indeed, adding ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," can even improve (!) the latency you have. You may wonder what happened with benchmarks we showed above after we added ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," option. In Centrifugo we chose default value 100 microseconds, and here are results on localhost (",(0,t.jsx)(n.code,{children:"old"})," without delay, ",(0,t.jsx)(n.code,{children:"new"})," with delay):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"> benchstat rueidis_p128.txt rueidis_delay_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 559ns \xb1 1% 468ns \xb1 0% -16.35% (p=0.000 n=9+8)\nRedisPublish_History-8 9.72\xb5s \xb1 1% 9.67\xb5s \xb1 1% -0.52% (p=0.007 n=9+8)\nRedisSubscribe-8 1.45\xb5s \xb1 1% 1.27\xb5s \xb1 1% -12.49% (p=0.000 n=9+10)\nRedisRecover-8 6.25\xb5s \xb1 1% 5.85\xb5s \xb1 0% -6.32% (p=0.000 n=10+10)\nRedisAddPresence-8 3.60\xb5s \xb1 1% 3.33\xb5s \xb1 1% -7.52% (p=0.000 n=10+10)\n\n(rest is not important here...)\n"})}),"\n",(0,t.jsx)(n.p,{children:"It's even better for this set of benchmarks. Though while it's better for these benchmarks the numbers may differ for other under different conditions. For example, in the benchmarks we run we use concurrency 128, if we reduce concurrency we will notice reduced throughput \u2013 as batches Rueidis collects become smaller. Smaller batches + some delay to collect = less requests per second to send."}),"\n",(0,t.jsxs)(n.p,{children:["The problem is that the value to pause Rueidis write loop is a very use case specific, it's pretty hard to provide a reasonable default for it. Depending on request rate/size, network latency etc. you may choose a larger or smaller delay. In v4.1.0 we start with hardcoded 100 microsecond ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," which seems sufficient for most use cases and showed good results in the benchmarks - though possibly we will have to make it tunable later."]}),"\n",(0,t.jsxs)(n.p,{children:["To check that Centrifugo benchmarks also utilize less CPU I added rate limiter (50k rps per second) to benchmarks and compared version without ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," and with 100 microsecond ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"}),":"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"50k req per second"}),(0,t.jsx)(n.th,{children:"Without delay"}),(0,t.jsx)(n.th,{children:"With 100mks delay"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPublish"}),(0,t.jsx)(n.td,{children:"Centrifugo - 75%, Redis - 24%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 44%, Redis - 9%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPublish_History"}),(0,t.jsx)(n.td,{children:"Centrifugo - 80% , Redis - 67%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 55%, Redis - 50%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkSubscribe"}),(0,t.jsx)(n.td,{children:"Centrifugo - 80%, Redis - 30%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 45% , Redis - 14%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkRecover"}),(0,t.jsx)(n.td,{children:"Centrifugo - 84%, Redis - 51%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 51%, Redis - 36%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPresence"}),(0,t.jsx)(n.td,{children:"Centrifugo - 114%, Redis - 69%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 90%, Redis - 60%"})]})]})]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:["In this test I replaced ",(0,t.jsx)(n.code,{children:"BenchmarkAddPresence"})," with ",(0,t.jsx)(n.code,{children:"BenchmarkPresence"})," (get information about all online subscribers in channel) to also make sure we have CPU reduction when using read-intensive method, i.e. when Redis response is reasonably large."]})}),"\n",(0,t.jsx)(n.p,{children:"We observe a notable CPU usage improvement here."}),"\n",(0,t.jsxs)(n.p,{children:["Hope you understand now why increasing ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," value in the pipelining code showed before results into increased CPU usage on app and Redis sides \u2013 due to smaller batch sizes and more read/write system calls as the consequence."]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsx)(n.p,{children:"BTW, would it be a nice thing if Go benchmarking suite could show a CPU usage of the process in addition to time and alloc stats? \ud83e\udd14"})}),"\n",(0,t.jsx)(n.h2,{id:"adding-latency",children:"Adding latency"}),"\n",(0,t.jsxs)(n.p,{children:["The last thing to check is how new implementation works upon increased RTT between application and Redis. To add artificial latency on localhost on Linux one can use ",(0,t.jsx)(n.code,{children:"tc"})," tool as ",(0,t.jsx)(n.a,{href:"https://daniel.haxx.se/blog/2010/12/14/add-latency-to-localhost/",children:"shown here"})," by Daniel Stenberg. But I am on MacOS so the simplest way I found was using ",(0,t.jsx)(n.a,{href:"https://github.com/Shopify/toxiproxy",children:"Shopify/toxiproxy"}),". Sth like running a server:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"toxyproxy-server\n"})}),"\n",(0,t.jsxs)(n.p,{children:["And then in another terminal I used ",(0,t.jsx)(n.code,{children:"toxiproxy-cli"})," to create toxic Redis proxy with additional latency on port 26379:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"toxiproxy-cli create -l localhost:26379 -u localhost:6379 toxic_redis\ntoxiproxy-cli toxic add -t latency -a latency=5 toxic_redis\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The benchmark results are (",(0,t.jsx)(n.code,{children:"old"})," is Redigo-based, new is Rueidis-based):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"> benchstat redigo_latency_p128.txt rueidis_delay_latency_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 31.5\xb5s \xb1 1% 5.6\xb5s \xb1 3% -82.26% (p=0.000 n=9+10)\nRedisPublish_History-8 62.8\xb5s \xb1 3% 10.6\xb5s \xb1 4% -83.05% (p=0.000 n=10+10)\nRedisSubscribe-8 1.52\xb5s \xb1 5% 6.05\xb5s \xb1 8% +298.70% (p=0.000 n=8+10)\nRedisRecover-8 48.3\xb5s \xb1 3% 7.3\xb5s \xb1 4% -84.80% (p=0.000 n=10+10)\nRedisAddPresence-8 52.3\xb5s \xb1 4% 5.8\xb5s \xb1 2% -88.94% (p=0.000 n=10+10)\n\n(rest is not important here...)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["We see that new Engine implementation behaves much better for most cases. But what happened to ",(0,t.jsx)(n.code,{children:"Subscribe"})," operation? It did not change at all in Redigo case \u2013 the same performance as if there is no additional latency involved!"]}),"\n",(0,t.jsxs)(n.p,{children:["Turned out that when we call ",(0,t.jsx)(n.code,{children:"Subscribe"})," in Redigo case, Redigo only flushes data to the network without waiting synchronously for subscribe result."]}),"\n",(0,t.jsx)(n.p,{children:"It makes sense in general and we can listen to subscribe notifications asynchronously, but in Centrifugo we relied on the returned error thinking that it includes succesful subscription result from Redis - meaning that we already subscribed to a channel at that point. And this could theoretically lead to some rare bugs in Centrifugo."}),"\n",(0,t.jsxs)(n.p,{children:["Rueidis library waits for subscribe response. So here the behavior of ",(0,t.jsx)(n.code,{children:"rueidis"})," while differs from ",(0,t.jsx)(n.code,{children:"redigo"})," in terms of throughput under increased latency just fits Centrifugo better in terms of behavior. So we go with it."]}),"\n",(0,t.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,t.jsx)(n.p,{children:"Migrating from Redigo to Rueidis library was not just a task of rewriting code, we had to carefully test various aspects of Redis Engine behaviour \u2013 latency, throughput, CPU utilization of application, and even CPU utilization of Redis itself under the equal application load conditions."}),"\n",(0,t.jsxs)(n.p,{children:["I think that we will find more projects in Go ecosystem using ",(0,t.jsx)(n.code,{children:"rueidis"})," library shortly. Not just because of its allocation efficiency and out-of-the-box throughput, but also due to a convenient type-safe command API."]}),"\n",(0,t.jsx)(n.p,{children:"For most Centrifugo users this migration means more efficient CPU usage as new implementation allocates less memory (less work to allocate and less strain on GC) and we tried to find a reasonable batch size to reduce the number of system calls for common operations. While latency and throughput of single Centrifugo node should be better as we make concurrent Redis calls from many goroutines."}),"\n",(0,t.jsx)(n.p,{children:"Hopefully readers will learn some tips from this post which can help to achieve effective communication with Redis from Go or another programming language."}),"\n",(0,t.jsx)(n.p,{children:"A few key takeaways:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Redis pipelining may increase throughput and reduce latency, it can also reduce CPU utilization of Redis"}),"\n",(0,t.jsx)(n.li,{children:"Don't blindly trust Go benchmark numbers but also think about CPU effect of changes you made (sometimes of the external system also)"}),"\n",(0,t.jsx)(n.li,{children:"Reduce the number of system calls to decrease CPU utilization"}),"\n",(0,t.jsx)(n.li,{children:"Everything is a trade-off \u2013 latency or resource usage? Your own WebSocket server or Centrifugo?"}),"\n",(0,t.jsxs)(n.li,{children:["Don't rely on someone's else benchmarks, including those published here. ",(0,t.jsx)(n.strong,{children:"Measure for your own use case"}),". Take into account your load profile, paralellism, network latency, data size, etc."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["P.S. One thing worth mentioning and which may be helpful for someone is that during our comparison experiments we discovered that Redis 7 has a major latency increase compared to Redis 6 when executing Lua scripts. So if you have performance sensitive code with Lua scripts take a look at ",(0,t.jsx)(n.a,{href:"https://github.com/redis/redis/issues/10981",children:"this Redis issue"}),". With the help of Redis developers some things already improved in ",(0,t.jsx)(n.code,{children:"unstable"})," Redis branch, hopefully that issue will be closed at the time you read this post."]})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},44755:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/goredis_allocs-3b6ad3ecb2543d679e812619438962c3.png"},5443:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_cover-94601b44109b725188bcf75df0fb520c.png"},79696:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_pipeline-7fdadb07739a87c92e520d0fba06696d.png"},13539:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_vis01-8364cd7474502a278b9362d5f5e71245.png"},99442:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_vis02-a8e078e75bac446b0c907beeeaf368ae.png"},91177:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/rueidis_1-05b9a03175ec64b4ec70679313bd4a38.png"},23522:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/rueidis_2-55b7b75cf531852fed8f73ceb7883b4d.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>a,a:()=>r});var t=i(67294);const s={},o=t.createContext(s);function r(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4ebb2955.c54e9f74.js b/assets/js/4ebb2955.c54e9f74.js new file mode 100644 index 000000000..023124798 --- /dev/null +++ b/assets/js/4ebb2955.c54e9f74.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3190],{90368:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var t=i(85893),s=i(11151);const o={title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",tags:["centrifugo","redis","go"],description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/redis_cover.png",hide_table_of_contents:!1},r=void 0,a={permalink:"/blog/2022/12/20/improving-redis-engine-performance",editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/blog/2022-12-20-improving-redis-engine-performance.md",source:"@site/blog/2022-12-20-improving-redis-engine-performance.md",title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",date:"2022-12-20T00:00:00.000Z",tags:[{label:"centrifugo",permalink:"/blog/tags/centrifugo"},{label:"redis",permalink:"/blog/tags/redis"},{label:"go",permalink:"/blog/tags/go"}],readingTime:28.995,hasTruncateMarker:!0,authors:[{name:"Alexander Emelin",title:"Author of Centrifugo",imageURL:"https://github.com/FZambia.png"}],frontMatter:{title:"Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",tags:["centrifugo","redis","go"],description:"In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",author:"Alexander Emelin",authorTitle:"Author of Centrifugo",authorImageURL:"https://github.com/FZambia.png",image:"/img/redis_cover.png",hide_table_of_contents:!1},unlisted:!1,prevItem:{title:"Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket",permalink:"/blog/2023/03/31/keycloak-sso-centrifugo"},nextItem:{title:"101 ways to subscribe user on a personal channel in Centrifugo",permalink:"/blog/2022/07/29/101-way-to-subscribe"}},d={authorsImageUrls:[void 0]},l=[{value:"Broker and PresenceManager",id:"broker-and-presencemanager",level:2},{value:"Redigo",id:"redigo",level:2},{value:"Redigo with pipelining",id:"redigo-with-pipelining",level:2},{value:"Motivation to migrate",id:"motivation-to-migrate",level:2},{value:"Go-redis/redis",id:"go-redisredis",level:2},{value:"Rueidis",id:"rueidis",level:2},{value:"Switching to Rueidis: reducing CPU usage",id:"switching-to-rueidis-reducing-cpu-usage",level:2},{value:"Adding latency",id:"adding-latency",level:2},{value:"Conclusion",id:"conclusion",level:2}];function c(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",h2:"h2",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Centrifugo_Redis_Engine_Improvements",src:i(78811).Z+"",width:"1200",height:"369"})}),"\n",(0,t.jsxs)(n.p,{children:["The main objective of Centrifugo is to manage persistent client connections established over various real-time transports (including WebSocket, HTTP-Streaming, SSE, WebTransport, etc \u2013 see ",(0,t.jsx)(n.a,{href:"https://centrifugal.dev/docs/transports/overview",children:"here"}),") and offer an API for publishing data towards established connections. Clients subscribe to channels, hence Centrifugo implements PUB/SUB mechanics to transmit published data to all online channel subscribers."]}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo employs ",(0,t.jsx)(n.a,{href:"https://redis.com/",children:"Redis"})," as its primary scalability option \u2013 so that it's possible to distribute client connections amongst numerous Centrifugo nodes without worrying about channel subscribers connected to separate nodes. Redis is incredibly mature, simple, and fast in-memory storage. Due to various built-in data structures and PUB/SUB support Redis is a perfect fit to be both Centrifugo ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," (we will describe what's this shortly)."]}),"\n",(0,t.jsxs)(n.p,{children:["In Centrifugo v4.1.0 we introduced an updated implementation of our Redis Engine (",(0,t.jsx)(n.code,{children:"Engine"})," in Centrifugo == ",(0,t.jsx)(n.code,{children:"Broker"})," + ",(0,t.jsx)(n.code,{children:"PresenceManager"}),") which provides sufficient performance improvements to our users. This post discusses the factors that prompted us to update Redis Engine implementation and provides some insight into the results we managed to achieve. We'll examine a few well-known Go libraries for Redis communication and contrast them against Centrifugo tasks."]}),"\n",(0,t.jsx)(n.h2,{id:"broker-and-presencemanager",children:"Broker and PresenceManager"}),"\n",(0,t.jsxs)(n.p,{children:["Before we get started, let's define what Centrifugo's ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," terms mean."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L97",children:"Broker"})," is an interface responsible for maintaining subscriptions from different Centrifugo nodes (initiated by client connections). That helps to scale client connections over many Centrifugo instances and not worry about the same channel subscribers being connected to different nodes \u2013 since all Centrifugo nodes connected with PUB/SUB. Messages published to one node are delivered to a channel subscriber connected to another node."]}),"\n",(0,t.jsxs)(n.p,{children:["Another major part of ",(0,t.jsx)(n.code,{children:"Broker"})," is keeping an expiring publication history for channels (streams). So that Centrifugo may provide a fast cache for messages missed by clients upon going offline for a short period and compensate at most once delivery of Redis PUB/SUB using ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L9",children:"Publication"})," incremental offsets. Centrifugo uses STREAM and HASH data structures in Redis to store channel history and stream meta information."]}),"\n",(0,t.jsxs)(n.p,{children:["In general Centrifugo architecture may be perfectly illustrated by this picture (Gophers are Centrifugo nodes all connected to ",(0,t.jsx)(n.code,{children:"Broker"}),", and sockets are WebSockets):"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:"https://i.imgur.com/QOJ1M9a.png",alt:"gopher-broker"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/presence.go#L12",children:"PresenceManager"})," is an interface responsible for managing online presence information - list of currently active channel subscribers. While the connection is alive we periodically update presence entries for channels connection subscribed to (for channels where presence is enabled). Presence data should expire if not updated by a client connection for some time. Centrifugo uses two Redis data structures for managing presence in channels - HASH and ZSET."]}),"\n",(0,t.jsx)(n.h2,{id:"redigo",children:"Redigo"}),"\n",(0,t.jsxs)(n.p,{children:["For a long time, the ",(0,t.jsx)(n.a,{href:"https://github.com/gomodule/redigo",children:"gomodule/redigo"})," package served as the foundation for the Redis Engine implementation in Centrifugo. Huge props go to ",(0,t.jsx)(n.a,{href:"https://github.com/garyburd",children:"Mr Gary Burd"})," for creating it."]}),"\n",(0,t.jsxs)(n.p,{children:["Redigo offers a connection ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/gomodule/redigo/redis#Pool",children:"Pool"})," to Redis. A simple usage of it involves getting the connection from the pool, issuing request to Redis over that connection, and then putting the connection back to the pool after receiving the result from Redis."]}),"\n",(0,t.jsx)(n.p,{children:"Let's write a simple benchmark which demonstrates simple usage of Redigo and measures SET operation performance:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkRedigo(b *testing.B) {\n\tpool := redigo.Pool{\n\t\tMaxIdle: 128,\n\t\tMaxActive: 128,\n\t\tWait: true,\n\t\tDial: func() (redigo.Conn, error) {\n\t\t\treturn redigo.Dial("tcp", ":6379")\n\t\t},\n\t}\n\tdefer pool.Close()\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tc := pool.Get()\n\t\t\t_, err := c.Do("SET", "redigo", "test")\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t\tc.Close()\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Let's run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Seems pretty fast, but we can improve it further."}),"\n",(0,t.jsx)(n.h2,{id:"redigo-with-pipelining",children:"Redigo with pipelining"}),"\n",(0,t.jsxs)(n.p,{children:["To increase a throughput in Centrifugo, instead of using Redigo's ",(0,t.jsx)(n.code,{children:"Pool"})," for each operation, we acquired a dedicated connection from the ",(0,t.jsx)(n.code,{children:"Pool"})," and utilized ",(0,t.jsx)(n.a,{href:"https://redis.io/docs/manual/pipelining/",children:"Redis pipelining"})," to send multiple commands where possible."]}),"\n",(0,t.jsx)(n.p,{children:"Redis pipelining improves performance by executing multiple commands using a single client-server-client round trip. Instead of executing many commands one by one, you can queue the commands in a pipeline and then execute the queued commands as if it is a single command. Redis processes commands in order and sends individual response for each command. Given a single CPU nature of Redis, reducing the number of active connections when using pipelining has a positive impact on throughput \u2013 therefore pipelining is beneficial from this angle as well."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Redis pipeline",src:i(35577).Z+"",width:"2556",height:"1564"})}),"\n",(0,t.jsxs)(n.p,{children:["You can quickly estimate the benefits of pipelining by running Redis locally and running ",(0,t.jsx)(n.code,{children:"redis-benchmark"})," which comes with Redis distribution over it:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"> redis-benchmark -n 100000 set key value\n\nSummary:\n throughput summary: 84674.01 requests per second\n"})}),"\n",(0,t.jsx)(n.p,{children:"And with pipelining:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"> redis-benchmark -n 100000 -P 64 set key value\n\nSummary:\n throughput summary: 666880.00 requests per second\n"})}),"\n",(0,t.jsxs)(n.p,{children:["In Centrifugo we are using smart batching technique for collecting pipeline (also described in ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket",children:"one of the previous posts"})," in this blog)."]}),"\n",(0,t.jsx)(n.p,{children:"To demonstrate benefits from using pipelining let's look at the following benchmark:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'const (\n\tmaxCommandsInPipeline = 512\n\tnumPipelineWorkers = 1\n)\n\ntype command struct {\n\terrCh chan error\n}\n\ntype sender struct {\n\tcmdCh chan command\n\tpool redigo.Pool\n}\n\nfunc newSender(pool redigo.Pool) *sender {\n\tp := &sender{\n\t\tcmdCh: make(chan command),\n\t\tpool: pool,\n\t}\n\tgo func() {\n\t\tfor {\n\t\t\tfor i := 0; i < numPipelineWorkers; i++ {\n\t\t\t\tp.runPipelineRoutine()\n\t\t\t}\n\t\t}\n\t}()\n\treturn p\n}\n\nfunc (s *sender) send() error {\n\terrCh := make(chan error, 1)\n\tcmd := command{\n\t\terrCh: errCh,\n\t}\n\t// Submit command to be executed by runPipelineRoutine.\n\ts.cmdCh <- cmd\n\treturn <-errCh\n}\n\nfunc (s *sender) runPipelineRoutine() {\n\tconn := p.pool.Get()\n\tdefer conn.Close()\n\tfor {\n\t\tselect {\n\t\tcase cmd := <-s.cmdCh:\n\t\t\tcommands := []command{cmd}\n\t\t\tconn.Send("set", "redigo", "test")\n\t\tloop:\n\t\t\t// Collect batch of commands to send to Redis in one RTT.\n\t\t\tfor i := 0; i < maxCommandsInPipeline; i++ {\n\t\t\t\tselect {\n\t\t\t\tcase cmd := <-s.cmdCh:\n\t\t\t\t\tcommands = append(commands, cmd)\n\t\t\t\t\tconn.Send("set", "redigo", "test")\n\t\t\t\tdefault:\n\t\t\t\t\tbreak loop\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush all collected commands to the network.\n\t\t\terr := conn.Flush()\n\t\t\tif err != nil {\n\t\t\t\tfor i := 0; i < len(commands); i++ {\n\t\t\t\t\tcommands[i].errCh <- err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Read responses to commands, they come in order.\n\t\t\tfor i := 0; i < len(commands); i++ {\n\t\t\t\t_, err := conn.Receive()\n\t\t\t\tcommands[i].errCh <- err\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc BenchmarkRedigoPipelininig(b *testing.B) {\n\tpool := redigo.Pool{\n\t\tWait: true,\n\t\tDial: func() (redigo.Conn, error) {\n\t\t\treturn redigo.Dial("tcp", ":6379")\n\t\t},\n\t}\n\tdefer pool.Close()\n\n\tsender := newSender(pool)\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\terr := sender.send()\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"This is a strategy that we employed in Centrifugo for a long time. As you can see code with automatic pipelining gets more complex, and in real life it's even more complicated to support different types of commands, channel send timeouts, and server shutdowns."}),"\n",(0,t.jsx)(n.p,{children:"What about the performance of this approach?"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkRedigoPipelininig-8 1840758 604.7 ns/op 176 B/op 4 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Operation latency reduced from 4648 ns/op to 604.7 ns/op \u2013 not bad right?"}),"\n",(0,t.jsxs)(n.p,{children:["It's worth mentioning that upon increased RTT between application and Redis the approach with pipelining will provide worse throughput. But it still can be better than in pool-based approach. Let's say we have latency 5ms between app and Redis. This means that with pool size of 128 you will be able to issue up to ",(0,t.jsx)(n.code,{children:"128 * (1000 / 5) = 25600"})," requests per second over 128 connections. With the pipelining approach above the theoretical limit is ",(0,t.jsx)(n.code,{children:"512 * (1000 / 5) = 102400"})," requests per second over a single connection (though in case of using code for pipelining shown above we need to have larger parallelism, say 512 instead of 128). And it can scale further if you increase ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," to work over several connections in paralell. Though increasing ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," has negative effect on CPU \u2013 we will discuss this later in this post."]}),"\n",(0,t.jsx)(n.p,{children:"Redigo is an awesome battle-tested library that served us great for a long time."}),"\n",(0,t.jsx)(n.h2,{id:"motivation-to-migrate",children:"Motivation to migrate"}),"\n",(0,t.jsx)(n.p,{children:"There are three modes in which Centrifugo can work with Redis these days:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Connecting to a standalone single Redis instance"}),"\n",(0,t.jsx)(n.li,{children:"Connecting to Redis in master-replica configuration, where Redis Sentinel controls the failover process"}),"\n",(0,t.jsx)(n.li,{children:"Connecting to Redis Cluster"}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"All modes additionally can be used with client-side consistent sharding. So it's possible to scale Redis even without a Redis Cluster setup."}),"\n",(0,t.jsx)(n.p,{children:"Unfortunately, with pure Redigo library, it's only possible to implement [ 1 ] \u2013 i.e. connecting to a single standalone Redis instance."}),"\n",(0,t.jsx)(n.p,{children:"To support the scheme with Sentinel you whether need to have a proxy between the application and Redis which proxies the connection to Redis master. For example, with Haproxy it's possible in this way:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"listen redis\n server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions\n server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup\n bind *:6379\n mode tcp\n option tcpka\n option tcplog\n option tcp-check\n tcp-check send PING\\r\\n\n tcp-check expect string +PONG\n tcp-check send info\\ replication\\r\\n\n tcp-check expect string role:master\n tcp-check send QUIT\\r\\n\n tcp-check expect string +OK\n balance roundrobin\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Or, you need to additionally import ",(0,t.jsx)(n.a,{href:"https://github.com/FZambia/sentinel",children:"FZambia/sentinel"})," library - which provides a communication layer with Redis Sentinel on top of Redigo's connection Pool."]}),"\n",(0,t.jsxs)(n.p,{children:["For communicating with Redis Cluster one more library may be used \u2013 ",(0,t.jsx)(n.a,{href:"https://github.com/mna/redisc",children:"mna/redisc"})," which is also a layer on top of ",(0,t.jsx)(n.code,{children:"redigo"})," basic functionality."]}),"\n",(0,t.jsxs)(n.p,{children:["Combining ",(0,t.jsx)(n.code,{children:"redigo"})," + ",(0,t.jsx)(n.code,{children:"FZambia/sentinel"})," + ",(0,t.jsx)(n.code,{children:"mna/redisc"})," we managed to implement all three connection modes. This worked, though resulted in rather tricky Redis setup. Also, it was difficult to re-use existing pipelining code we had for a standalone Redis with Redis Cluster. As a result, Centrifugo only used pipelining in a standalone or Sentinel Redis cases. When using Redis Cluster, however, Centrifugo merely used the connection pool to issue requests thus not benefiting from request pipelining. Due to this we had some code duplication to send the same requests in various Redis configurations."]}),"\n",(0,t.jsxs)(n.p,{children:["Another thing is that Redigo uses ",(0,t.jsx)(n.code,{children:"interface{}"})," for command construction. To send command to Redis Redigo has ",(0,t.jsx)(n.code,{children:"Do"})," method which accepts name of the command and variadic ",(0,t.jsx)(n.code,{children:"interface{}"})," arguments to construct command arguments:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"Do(commandName string, args ...interface{}) (reply interface{}, err error)\n"})}),"\n",(0,t.jsx)(n.p,{children:"While this works well and you can issue any command to Redis, you need to be very accurate when constructing a command. This also adds some allocation overhead. As we know more memory allocations lead to the increased CPU utilization because the allocation process itself requires more processing power and the GC is under more strain."}),"\n",(0,t.jsxs)(n.p,{children:["At some point we felt that eliminating additional dependencies (even though I am the author of one of them) and reducing allocations in Redis communication layer is a nice step forward for Centrifugo. So we started looking around for ",(0,t.jsx)(n.code,{children:"redigo"})," alternatives."]}),"\n",(0,t.jsx)(n.p,{children:"To summarize, here is what we wanted from Redis library:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Possibility to work with all three Redis setup options we support: standalone, master-replica(s) with Sentinel, Redis Cluster, so we can depend on one library instead of three"}),"\n",(0,t.jsx)(n.li,{children:"Less memory allocations (and more type-safety API is a plus)"}),"\n",(0,t.jsx)(n.li,{children:"Support working with RESP2-only Redis servers as we need that for backwards compatibility. And some vendors like Redis Enterprise still support RESP2 protocol only"}),"\n",(0,t.jsx)(n.li,{children:"The library should be actively maintained"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"go-redisredis",children:"Go-redis/redis"}),"\n",(0,t.jsxs)(n.p,{children:["The most obvious alternative to Redigo is ",(0,t.jsx)(n.a,{href:"https://github.com/go-redis/redis",children:"go-redis/redis"})," package. It's popular, regularly gets updates, used by a huge amount of Go projects (Grafana, Thanos, etc.). And maintained by\n",(0,t.jsx)(n.a,{href:"https://github.com/vmihailenco",children:"Vladimir Mihailenco"})," who created several more awesome Go libraries, like ",(0,t.jsx)(n.a,{href:"https://github.com/vmihailenco/msgpack",children:"msgpack"})," for example. I personally successfully used ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in several other projects I worked on."]}),"\n",(0,t.jsxs)(n.p,{children:["To avoid setup boilerplate for various Redis installation variations ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," has ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#UniversalClient",children:"UniversalClient"}),". From docs:"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"UniversalClient is a wrapper client which, based on the provided options, represents either a ClusterClient, a FailoverClient, or a single-node Client. This can be useful for testing cluster-specific applications locally or having different clients in different environments."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In terms of implementation ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," also has internal pool of connections to Redis, similar to ",(0,t.jsx)(n.code,{children:"redigo"}),". It's also possible to use ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Pipeline",children:"Client.Pipeline"})," method to allocate a ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Pipeliner",children:"Pipeliner"})," interface and use it for pipelining. So ",(0,t.jsx)(n.code,{children:"UniversalClient"})," reduces setup boilerplate for different Redis installation types and number of dependencies we had, and it provide very similar way to pipeline requests so we could easily re-implement things we had with Redigo."]}),"\n",(0,t.jsxs)(n.p,{children:["Go-redis also provides more type-safety when constructing commands compared to Redigo, almost every command in Redis is implemented as a separate method of ",(0,t.jsx)(n.code,{children:"Client"}),", for example ",(0,t.jsx)(n.code,{children:"Publish"})," ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Publish",children:"defined"})," as:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"func (c Client) Publish(ctx context.Context, channel string, message interface{}) *IntCmd\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can see though that we still have ",(0,t.jsx)(n.code,{children:"interface{}"})," here for ",(0,t.jsx)(n.code,{children:"message"})," argument type. I suppose this was implemented in such way for convenience \u2013 to pass both ",(0,t.jsx)(n.code,{children:"string"})," or ",(0,t.jsx)(n.code,{children:"[]byte"}),". But it still produces some extra allocations."]}),"\n",(0,t.jsxs)(n.p,{children:["Without pipelining the simplest program with ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," may look like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkGoredis(b *testing.B) {\n\tclient := redis.NewUniversalClient(&redis.UniversalOptions{\n\t\tAddrs: []string{":6379"},\n\t\tPoolSize: 128,\n\t})\n\tdefer client.Close()\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tresp := client.Set(context.Background(), "goredis", "test", 0)\n\t\t\tif resp.Err() != nil {\n\t\t\t\tb.Fatal(resp.Err())\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"Let's run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkGoredis-8 268444 4561 ns/op 244 B/op 8 allocs/op\n"})}),"\n",(0,t.jsx)(n.p,{children:"Result is pretty comparable to Redigo, though Go-redis allocates more (btw most of allocations come from the connection liveness check upon getting from the pool which can not be turned off)."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(29399).Z+"",width:"1884",height:"986"})}),"\n",(0,t.jsxs)(n.p,{children:["It's interesting \u2013 if we dive deeper into what is it we can discover that this is the only way in Go to check connection was closed without reading data from it. The approach was originally introduced ",(0,t.jsx)(n.a,{href:"https://github.com/go-sql-driver/mysql/blob/41dd159e6ec9afad00d2b90144bbc083ea860db1/conncheck.go#L23",children:"by go-sql-driver/mysql"}),", it's not cross-platform, and ",(0,t.jsx)(n.a,{href:"https://github.com/golang/go/issues/15735",children:"related issue"})," may be found in Go issue tracker."]}),"\n",(0,t.jsxs)(n.p,{children:["But as I said in Centrifugo we already used pipelining over the dedicated connection for all operations so we avoid frequently getting connections from the pool. And early experiments proved that ",(0,t.jsx)(n.code,{children:"go-redis"})," may provide some performance benefits for our use case."]}),"\n",(0,t.jsxs)(n.p,{children:["At some point ",(0,t.jsx)(n.a,{href:"https://github.com/j178",children:"@j178"})," sent ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/pull/235",children:"a pull request"})," to Centrifuge library with ",(0,t.jsx)(n.code,{children:"Broker"})," and ",(0,t.jsx)(n.code,{children:"PresenceManager"})," implementations based on ",(0,t.jsx)(n.code,{children:"go-redis/redis"}),". The amount of code to cover all the various Redis setups was reduced, we got only one dependency instead of three \ud83d\udd25"]}),"\n",(0,t.jsx)(n.p,{children:"But what about performance? Here we will show results for several operations which are typical for Centrifugo:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Publish a message to a channel without saving it to the history - this is just a Redis PUBLISH command going through Redis PUB/SUB system (",(0,t.jsx)(n.code,{children:"RedisPublish"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Publish message to a channel with saving it to history - this involves executing the LUA script on Redis side where we add a publication to STREAM data structure, update meta information HASH, and finally PUBLISH to PUB/SUB (",(0,t.jsx)(n.code,{children:"RedisPublish_History"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Subscribe to a channel - that's a SUBSCRIBE Redis command, this is important to have it fast as Centrifugo should be able to re-subscribe to all the channels in the system upon ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"mass client reconnect scenario"})," (",(0,t.jsx)(n.code,{children:"RedisSubscribe"}),")"]}),"\n",(0,t.jsxs)(n.li,{children:["Recovering missed publication state from channel STREAM, this is again may be called lots of times when all clients reconnect at once (",(0,t.jsx)(n.code,{children:"RedisRecover"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Updating connection presence information - many connections may periodically update their channel online presence information in Redis (",(0,t.jsx)(n.code,{children:"RedisAddPresence"}),")"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Here are the benchmark results we got when comparing ",(0,t.jsx)(n.code,{children:"redigo"})," (v1.8.9) implementation (old) and ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," (v9.0.0-rc.2) implementation (new) with Redis v6.2.7 on Mac with M1 processor and benchmark paralellism 128:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"\u276f benchstat redigo_p128.txt goredis_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 1.45\xb5s \xb110% 1.88\xb5s \xb1 4% +29.32% (p=0.000 n=10+10)\nRedisPublish_History-8 12.5\xb5s \xb1 6% 9.7\xb5s \xb1 3% -22.77% (p=0.000 n=10+10)\nRedisSubscribe-8 1.47\xb5s \xb124% 1.47\xb5s \xb110% ~ (p=0.469 n=10+10)\nRedisRecover-8 18.4\xb5s \xb1 2% 6.3\xb5s \xb1 0% -65.78% (p=0.000 n=10+8)\nRedisAddPresence-8 3.72\xb5s \xb1 1% 3.40\xb5s \xb1 1% -8.74% (p=0.000 n=10+10)\n\nname old alloc/op new alloc/op delta\nRedisPublish-8 483B \xb1 0% 499B \xb1 0% +3.37% (p=0.000 n=9+10)\nRedisPublish_History-8 1.30kB \xb1 0% 1.08kB \xb1 0% -16.67% (p=0.000 n=10+10)\nRedisSubscribe-8 892B \xb1 2% 662B \xb1 6% -25.83% (p=0.000 n=10+10)\nRedisRecover-8 1.25kB \xb1 1% 1.00kB \xb1 0% -19.91% (p=0.000 n=10+10)\nRedisAddPresence-8 907B \xb1 0% 827B \xb1 0% -8.82% (p=0.002 n=7+8)\n\nname old allocs/op new allocs/op delta\nRedisPublish-8 10.0 \xb1 0% 9.0 \xb1 0% -10.00% (p=0.000 n=10+10)\nRedisPublish_History-8 29.0 \xb1 0% 25.0 \xb1 0% -13.79% (p=0.000 n=10+10)\nRedisSubscribe-8 22.0 \xb1 0% 14.0 \xb1 0% -36.36% (p=0.000 n=8+7)\nRedisRecover-8 29.0 \xb1 0% 23.0 \xb1 0% -20.69% (p=0.000 n=10+10)\nRedisAddPresence-8 18.0 \xb1 0% 17.0 \xb1 0% -5.56% (p=0.000 n=10+10)\n"})}),"\n",(0,t.jsx)(n.admonition,{type:"danger",children:(0,t.jsx)(n.p,{children:"Please note that this benchmark is not a pure performance comparison of two Go libraries for Redis \u2013 it's a performance comparison of Centrifugo Engine methods upon switching to a new library."})}),"\n",(0,t.jsx)(n.p,{children:"Or visualized in Grafana:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(64169).Z+"",width:"2268",height:"1336"})}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:["Centrifugo benchmarks results shown in the post use parallelism 128. If someone interested to check numbers for paralellism 1 or 16 \u2013 ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugal.dev/pull/18#issuecomment-1356263272",children:"check out this comment on Github"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"We observe a noticeable reduction in allocations in these benchmarks and in most benchmarks (presented here and other not listed in this post) we observed a reduced latency."}),"\n",(0,t.jsxs)(n.p,{children:["Overall, results convinced us that the migration from ",(0,t.jsx)(n.code,{children:"redigo"})," to ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," may provide Centrifugo with everything we aimed for \u2013 all the goals for a ",(0,t.jsx)(n.code,{children:"redigo"})," alternative outlined above were successfully fullfilled."]}),"\n",(0,t.jsxs)(n.p,{children:["One good thing ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," allowed us to do is to use Redis pipelining also in a Redis Cluster case. It's possible due to the fact that ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," ",(0,t.jsx)(n.a,{href:"https://github.com/go-redis/redis/blob/c561f3ca7e5cf44ce1f1d3ef30f4a10a9c674c8a/cluster.go#L1062",children:"re-maps pipeline objects internally"})," based on keys to execute pipeline on the correct node of Redis Cluster. Actually, we could do the same based on ",(0,t.jsx)(n.code,{children:"redigo"})," + ",(0,t.jsx)(n.code,{children:"mna/redisc"}),", but here we got it for free."]}),"\n",(0,t.jsxs)(n.p,{children:["BTW, there is ",(0,t.jsx)(n.a,{href:"https://redis.uptrace.dev/guide/go-redis-vs-redigo.html",children:"a page with comparison"})," between ",(0,t.jsx)(n.code,{children:"redigo"})," and ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," docs which outlines some things I mentioned here and some others."]}),"\n",(0,t.jsxs)(n.p,{children:["But we have not migrated to ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in the end. And the reason is another library \u2013 ",(0,t.jsx)(n.code,{children:"rueidis"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"rueidis",children:"Rueidis"}),"\n",(0,t.jsxs)(n.p,{children:["While results were good with ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," we also made an attempt to implement Redis Engine on top of ",(0,t.jsx)(n.a,{href:"https://github.com/rueian/rueidis",children:"rueian/rueidis"})," library written by ",(0,t.jsx)(n.a,{href:"https://github.com/rueian",children:"@rueian"}),". According to docs, ",(0,t.jsx)(n.code,{children:"rueidis"})," is:"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"A fast Golang Redis client that supports Client Side Caching, Auto Pipelining, Generics OM, RedisJSON, RedisBloom, RediSearch, RedisAI, RedisGears, etc."}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The readme of ",(0,t.jsx)(n.code,{children:"rueidis"})," contains benchmark results where it hugely outperforms ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," in terms of operation latency/throughput in both single Redis and Redis Custer setups:"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(6101).Z+"",width:"2846",height:"1448"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(88457).Z+"",width:"2846",height:"1438"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"rueidis"})," works with standalone Redis, Sentinel Redis and Redis Cluster out of the box. Just like ",(0,t.jsx)(n.code,{children:"UniversalClient"})," of ",(0,t.jsx)(n.code,{children:"go-redis/redis"}),". So it also allowed us to reduce code boilerplate to work with all these setups."]}),"\n",(0,t.jsx)(n.p,{children:"Again, let's try to write a simple program like we had for Redigo and Go-redis above:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:'func BenchmarkRueidis(b *testing.B) {\n\tclient, err := rueidis.NewClient(rueidis.ClientOption{\n\t\tInitAddress: []string{":6379"},\n\t})\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tb.ResetTimer()\n\tb.SetParallelism(128)\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tcmd := client.B().Set().Key("rueidis").Value("test").Build()\n\t\t\tres := client.Do(context.Background(), cmd)\n\t\t\tif res.Error() != nil {\n\t\t\t\tb.Fatal(res.Error())\n\t\t\t}\n\t\t}\n\t})\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"And run it:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"BenchmarkRedigo-8 228804 4648 ns/op 62 B/op 2 allocs/op\nBenchmarkGoredis-8 268444 4561 ns/op 244 B/op 8 allocs/op\nBenchmarkRueidis-8 2908591 418.5 ns/op 4 B/op 1 allocs/op\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"rueidis"})," library comes with ",(0,t.jsx)(n.strong,{children:"automatic implicit pipelining"}),", so you can send each request in isolated way while ",(0,t.jsx)(n.code,{children:"rueidis"})," makes sure the request becomes part of the pipeline sent to Redis \u2013 thus utilizing the connection between an application and Redis most efficiently with maximized throughput. The idea of implicit pipelining with Redis is not new and Go ecosystem already had ",(0,t.jsx)(n.a,{href:"https://github.com/joomcode/redispipe",children:"joomcode/redispipe"})," library which implemented it (though it comes with some limitations which made it unsuitable for Centrifugo use case)."]}),"\n",(0,t.jsxs)(n.p,{children:["So ",(0,t.jsx)(n.strong,{children:"applications that use a pool-based approach"})," for communication with Redis may observe dramatic improvements in latency and throughput when switching to the Rueidis library."]}),"\n",(0,t.jsxs)(n.p,{children:["For Centrifugo we didn't expect such a huge speed-up as shown in the above graphs since we already used pipelining in Redis Engine. But ",(0,t.jsx)(n.code,{children:"rueidis"}),' implements some ideas which allow it to be efficient. Insights about these ideas are provided by Rueidis author in a "Writing a High-Performance Golang Client Library" series of posts on Medium:']}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/writing-high-performance-golang-client-library-part-1-batching-on-pipeline-97988fe3211",children:"Part 1: Batching on Pipeline"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/working-on-high-performance-golang-client-library-reading-again-from-channels-5e98ff3538cf",children:"Part 2: Reading Again From Channels?"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://betterprogramming.pub/working-on-high-performance-golang-client-library-remove-the-bad-busy-loops-with-the-sync-cond-e262b3fcb458",children:"Part 3: Remove the Bad Busy Loops With the Sync.Cond"})}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["I did some prototypes with ",(0,t.jsx)(n.code,{children:"rueidis"})," which were super-promising in terms of performance. There were some issues found during that early prototyping (mostly with PUB/SUB) \u2013 but all of them were quickly resolved by Rueian."]}),"\n",(0,t.jsxs)(n.p,{children:["Until ",(0,t.jsx)(n.code,{children:"v0.0.80"})," release ",(0,t.jsx)(n.code,{children:"rueidis"})," did not support RESP2 though, so we could not replace our Redis Engine implementation with it. But as soon as it got RESP2 support we opened ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifuge/pull/262",children:"a pull request with alternative implementation"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Since auto-pipelining is used in ",(0,t.jsx)(n.code,{children:"rueidis"})," by default we were able to remove some of our own pipelining management code \u2013 so the Engine implementation is more concise now. One more thing to mention is a simpler PUB/SUB code we were able to write with ",(0,t.jsx)(n.code,{children:"rueidis"}),". One example is that in ",(0,t.jsx)(n.code,{children:"redigo"})," case we had to periodically PING PUB/SUB connection to maintain it alive, ",(0,t.jsx)(n.code,{children:"rueidis"})," does this automatically."]}),"\n",(0,t.jsxs)(n.p,{children:["Regarding performance, here are the benchmark results we got when comparing ",(0,t.jsx)(n.code,{children:"redigo"})," (v1.8.9) implementation (old) and ",(0,t.jsx)(n.code,{children:"rueidis"})," (v0.0.90) implementation (new):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"\u276f benchstat redigo_p128.txt rueidis_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 1.45\xb5s \xb110% 0.56\xb5s \xb1 1% -61.53% (p=0.000 n=10+9)\nRedisPublish_History-8 12.5\xb5s \xb1 6% 9.7\xb5s \xb1 1% -22.43% (p=0.000 n=10+9)\nRedisSubscribe-8 1.47\xb5s \xb124% 1.45\xb5s \xb1 1% ~ (p=0.484 n=10+9)\nRedisRecover-8 18.4\xb5s \xb1 2% 6.2\xb5s \xb1 1% -66.08% (p=0.000 n=10+10)\nRedisAddPresence-8 3.72\xb5s \xb1 1% 3.60\xb5s \xb1 1% -3.34% (p=0.000 n=10+10)\n\nname old alloc/op new alloc/op delta\nRedisPublish-8 483B \xb1 0% 91B \xb1 0% -81.16% (p=0.000 n=9+10)\nRedisPublish_History-8 1.30kB \xb1 0% 0.39kB \xb1 0% -70.08% (p=0.000 n=10+8)\nRedisSubscribe-8 892B \xb1 2% 360B \xb1 0% -59.66% (p=0.000 n=10+10)\nRedisRecover-8 1.25kB \xb1 1% 0.36kB \xb1 1% -71.52% (p=0.000 n=10+10)\nRedisAddPresence-8 907B \xb1 0% 151B \xb1 1% -83.34% (p=0.000 n=7+9)\n\nname old allocs/op new allocs/op delta\nRedisPublish-8 10.0 \xb1 0% 2.0 \xb1 0% -80.00% (p=0.000 n=10+10)\nRedisPublish_History-8 29.0 \xb1 0% 10.0 \xb1 0% -65.52% (p=0.000 n=10+10)\nRedisSubscribe-8 22.0 \xb1 0% 6.0 \xb1 0% -72.73% (p=0.002 n=8+10)\nRedisRecover-8 29.0 \xb1 0% 7.0 \xb1 0% -75.86% (p=0.000 n=10+10)\nRedisAddPresence-8 18.0 \xb1 0% 3.0 \xb1 0% -83.33% (p=0.000 n=10+10)\n"})}),"\n",(0,t.jsx)(n.p,{children:"Or visualized in Grafana:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(88999).Z+"",width:"2278",height:"1326"})}),"\n",(0,t.jsx)(n.p,{children:"2.5x times more publication throughput than we had before! Instead of 700k publications/sec, we went towards 1.7 million publications/sec due to drastically decreased publish operation latency (1.45\xb5s -> 0.59\xb5s). This means that our previous Engine implementation under-utilized Redis, and Rueidis just pushes us towards Redis limits. The latency of most other operations is also reduced."}),"\n",(0,t.jsxs)(n.p,{children:['The allocation effectiveness of the implementation based on "rueidis" is best. As you can see ',(0,t.jsx)(n.code,{children:"rueidis"})," helped us to generate sufficiently fewer memory allocations for all our Redis operations. Allocation improvements directly affect Centrifugo node CPU usage. Though we will talk about CPU more later below."]}),"\n",(0,t.jsx)(n.p,{children:"For Redis Cluster case we also got benchmark results similar to the standalone Redis results above."}),"\n",(0,t.jsxs)(n.p,{children:["I might add that I enjoyed building commands with ",(0,t.jsx)(n.code,{children:"rueidis"}),". All Redis commands may be constructed using a builder approach. Rueidis comes with builders generated for all Redis commands. As an illustration, this is a process of building a PUBLISH Redis command:"]}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/rueidis_cmd.mp4"}),"\n",(0,t.jsx)(n.p,{children:"This drastically reduces a chance to make a stupid mistake while constructing a command. Instead of always opening Redis docs to see a command syntax it's now possible to just start typing - and quickly come to the complete command to send."}),"\n",(0,t.jsx)(n.h2,{id:"switching-to-rueidis-reducing-cpu-usage",children:"Switching to Rueidis: reducing CPU usage"}),"\n",(0,t.jsx)(n.p,{children:"After making all these benchmarks and implementing Engine in Rueidis I decided to check whether Centrifugo consumes less CPU with it. I expected a notable CPU reduction as Rueidis Engine implementation allocates much less than Redigo-based. Turned out it's not that simple."}),"\n",(0,t.jsx)(n.p,{children:"I ran Centrifugo with some artificial load and noticed that CPU consumption of the new implementation is actually... worse than we had with Redigo-based engine under equal conditions!\ud83d\ude29 But why?"}),"\n",(0,t.jsxs)(n.p,{children:["As I mentioned above Redis pipelining is a technique when several commands may be combined into one batch to send over the network. In case of automatic pipelining the size of generated batches start playing a crucial role in application and Redis CPU usage \u2013 since smaller command batches result into more read/write system calls to the kernel on both application and Redis server sides. That's why projects like ",(0,t.jsx)(n.a,{href:"https://github.com/twitter/twemproxy",children:"Twemproxy"})," which sit between app and Redis have sich a good effect on Redis CPU usage among other things."]}),"\n",(0,t.jsx)(n.p,{children:"As we have seen above, Rueidis provides a better throughput and latency, but it's more agressive in terms of flushing data to the network. So in its default configuration we get smaller batches under th equal conditions than we had before in our own pipelining implementation based on Redigo (shown in the beginning of this post)."}),"\n",(0,t.jsxs)(n.p,{children:["Luckily, there is an option in Rueidis called ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," which allows to slow down write loop a bit to give Rueidis a chance to collect more commands to send in one batch. When this option is used Rueidis will make a pause after each network flush not bigger than selected value of ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," (please note, that this is a delay after flushing collected pipeline commands, not an additional delay for each request). Using some reasonable value it's possible to drastically reduce both application and Redis CPU utilization."]}),"\n",(0,t.jsxs)(n.p,{children:["To demonstrate this I created a repo: ",(0,t.jsx)(n.a,{href:"https://github.com/FZambia/pipelines",children:"https://github.com/FZambia/pipelines"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["This repo contains three benchmarks where we use automatic pipelining: based on ",(0,t.jsx)(n.code,{children:"redigo"}),", based on ",(0,t.jsx)(n.code,{children:"go-redis/redis"})," and ",(0,t.jsx)(n.code,{children:"rueidis"}),". In these benchmarks we produce concurrent requests, but instead of pushing the system towards the limits we are limiting number of requests sent to Redis, so we put all libraries in equal conditions."]}),"\n",(0,t.jsxs)(n.p,{children:["To rate limit requests we are using ",(0,t.jsx)(n.a,{href:"https://github.com/uber-go/ratelimit",children:"uber-go/ratelimit"})," library. For example, to allow rate no more than 100k commands per second we can do sth like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-go",children:"rl := ratelimit.New(100, ratelimit.Per(time.Millisecond))\nfor {\n\trl.Take()\n\t...\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["We limit requests per second we could actually just write ",(0,t.jsx)(n.code,{children:"ratelimit.New(100000)"})," \u2013 but we aim to get a more smooth distribution of requests over time - so using millisecond resolution."]}),"\n",(0,t.jsx)(n.p,{children:"Let's run all the benchmarks in the default configuration:"}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/redis_b1.mp4"}),"\n",(0,t.jsx)(n.p,{children:"Average CPU usage during the test (a bit rough but enough for demonstration):"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{}),(0,t.jsx)(n.th,{children:"Redigo"}),(0,t.jsx)(n.th,{children:"Go-redis/redis"}),(0,t.jsx)(n.th,{children:"Rueidis"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Application CPU, %"}),(0,t.jsx)(n.td,{children:"95"}),(0,t.jsx)(n.td,{children:"99"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"red"},children:"116"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Redis CPU, %"}),(0,t.jsx)(n.td,{children:"36"}),(0,t.jsx)(n.td,{children:"35"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"red"},children:"42"})})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["OK, Rueidis-based implementation is the worst here despite of allocating less than others. So let's try to change this by setting ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," to sth like 100 microseconds:"]}),"\n",(0,t.jsx)("video",{width:"100%",loop:!0,autoPlay:"autoplay",muted:!0,controls:"",src:"/img/redis_b2.mp4"}),"\n",(0,t.jsx)(n.p,{children:"Now CPU usage is:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{}),(0,t.jsx)(n.th,{children:"Redigo"}),(0,t.jsx)(n.th,{children:"Go-redis/redis"}),(0,t.jsx)(n.th,{children:"Rueidis"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Application CPU, %"}),(0,t.jsx)(n.td,{children:"95"}),(0,t.jsx)(n.td,{children:"99"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"green"},children:"59"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Redis CPU, %"}),(0,t.jsx)(n.td,{children:"36"}),(0,t.jsx)(n.td,{children:"35"}),(0,t.jsx)(n.td,{children:(0,t.jsx)("span",{style:{color:"green"},children:"12"})})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"So we can achieve great CPU usage reduction. CPU went from 116% to 59% for the application side, and from 42% to only 12% for Redis! We are sacrificing latency though. Given the fact the CPU utilization reduction is very notable the trade-off is pretty fair."}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsx)(n.p,{children:"It's definitely possible to improve CPU usage in Redigo and Go-redis/redis cases too \u2013 using similar technique. But the goal here was to improve Rueidis-based engine implementation to make it comparable or better than our Redigo-based implementation in terms of CPU utilization."})}),"\n",(0,t.jsxs)(n.p,{children:["As you can see we were able to achieve better CPU results just by using 100 microseconds delay after each network flush. In real life, where we are not running Redis on localhost and have some network latency in between application and Redis, this delay should be insignificant at all. Indeed, adding ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," can even improve (!) the latency you have. You may wonder what happened with benchmarks we showed above after we added ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," option. In Centrifugo we chose default value 100 microseconds, and here are results on localhost (",(0,t.jsx)(n.code,{children:"old"})," without delay, ",(0,t.jsx)(n.code,{children:"new"})," with delay):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"> benchstat rueidis_p128.txt rueidis_delay_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 559ns \xb1 1% 468ns \xb1 0% -16.35% (p=0.000 n=9+8)\nRedisPublish_History-8 9.72\xb5s \xb1 1% 9.67\xb5s \xb1 1% -0.52% (p=0.007 n=9+8)\nRedisSubscribe-8 1.45\xb5s \xb1 1% 1.27\xb5s \xb1 1% -12.49% (p=0.000 n=9+10)\nRedisRecover-8 6.25\xb5s \xb1 1% 5.85\xb5s \xb1 0% -6.32% (p=0.000 n=10+10)\nRedisAddPresence-8 3.60\xb5s \xb1 1% 3.33\xb5s \xb1 1% -7.52% (p=0.000 n=10+10)\n\n(rest is not important here...)\n"})}),"\n",(0,t.jsx)(n.p,{children:"It's even better for this set of benchmarks. Though while it's better for these benchmarks the numbers may differ for other under different conditions. For example, in the benchmarks we run we use concurrency 128, if we reduce concurrency we will notice reduced throughput \u2013 as batches Rueidis collects become smaller. Smaller batches + some delay to collect = less requests per second to send."}),"\n",(0,t.jsxs)(n.p,{children:["The problem is that the value to pause Rueidis write loop is a very use case specific, it's pretty hard to provide a reasonable default for it. Depending on request rate/size, network latency etc. you may choose a larger or smaller delay. In v4.1.0 we start with hardcoded 100 microsecond ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," which seems sufficient for most use cases and showed good results in the benchmarks - though possibly we will have to make it tunable later."]}),"\n",(0,t.jsxs)(n.p,{children:["To check that Centrifugo benchmarks also utilize less CPU I added rate limiter (50k rps per second) to benchmarks and compared version without ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"})," and with 100 microsecond ",(0,t.jsx)(n.code,{children:"MaxFlushDelay"}),":"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"50k req per second"}),(0,t.jsx)(n.th,{children:"Without delay"}),(0,t.jsx)(n.th,{children:"With 100mks delay"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPublish"}),(0,t.jsx)(n.td,{children:"Centrifugo - 75%, Redis - 24%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 44%, Redis - 9%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPublish_History"}),(0,t.jsx)(n.td,{children:"Centrifugo - 80% , Redis - 67%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 55%, Redis - 50%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkSubscribe"}),(0,t.jsx)(n.td,{children:"Centrifugo - 80%, Redis - 30%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 45% , Redis - 14%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkRecover"}),(0,t.jsx)(n.td,{children:"Centrifugo - 84%, Redis - 51%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 51%, Redis - 36%"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"BenchmarkPresence"}),(0,t.jsx)(n.td,{children:"Centrifugo - 114%, Redis - 69%"}),(0,t.jsx)(n.td,{children:"Centrifugo - 90%, Redis - 60%"})]})]})]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:["In this test I replaced ",(0,t.jsx)(n.code,{children:"BenchmarkAddPresence"})," with ",(0,t.jsx)(n.code,{children:"BenchmarkPresence"})," (get information about all online subscribers in channel) to also make sure we have CPU reduction when using read-intensive method, i.e. when Redis response is reasonably large."]})}),"\n",(0,t.jsx)(n.p,{children:"We observe a notable CPU usage improvement here."}),"\n",(0,t.jsxs)(n.p,{children:["Hope you understand now why increasing ",(0,t.jsx)(n.code,{children:"numPipelineWorkers"})," value in the pipelining code showed before results into increased CPU usage on app and Redis sides \u2013 due to smaller batch sizes and more read/write system calls as the consequence."]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsx)(n.p,{children:"BTW, would it be a nice thing if Go benchmarking suite could show a CPU usage of the process in addition to time and alloc stats? \ud83e\udd14"})}),"\n",(0,t.jsx)(n.h2,{id:"adding-latency",children:"Adding latency"}),"\n",(0,t.jsxs)(n.p,{children:["The last thing to check is how new implementation works upon increased RTT between application and Redis. To add artificial latency on localhost on Linux one can use ",(0,t.jsx)(n.code,{children:"tc"})," tool as ",(0,t.jsx)(n.a,{href:"https://daniel.haxx.se/blog/2010/12/14/add-latency-to-localhost/",children:"shown here"})," by Daniel Stenberg. But I am on MacOS so the simplest way I found was using ",(0,t.jsx)(n.a,{href:"https://github.com/Shopify/toxiproxy",children:"Shopify/toxiproxy"}),". Sth like running a server:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"toxyproxy-server\n"})}),"\n",(0,t.jsxs)(n.p,{children:["And then in another terminal I used ",(0,t.jsx)(n.code,{children:"toxiproxy-cli"})," to create toxic Redis proxy with additional latency on port 26379:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"toxiproxy-cli create -l localhost:26379 -u localhost:6379 toxic_redis\ntoxiproxy-cli toxic add -t latency -a latency=5 toxic_redis\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The benchmark results are (",(0,t.jsx)(n.code,{children:"old"})," is Redigo-based, new is Rueidis-based):"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"> benchstat redigo_latency_p128.txt rueidis_delay_latency_p128.txt\nname old time/op new time/op delta\nRedisPublish-8 31.5\xb5s \xb1 1% 5.6\xb5s \xb1 3% -82.26% (p=0.000 n=9+10)\nRedisPublish_History-8 62.8\xb5s \xb1 3% 10.6\xb5s \xb1 4% -83.05% (p=0.000 n=10+10)\nRedisSubscribe-8 1.52\xb5s \xb1 5% 6.05\xb5s \xb1 8% +298.70% (p=0.000 n=8+10)\nRedisRecover-8 48.3\xb5s \xb1 3% 7.3\xb5s \xb1 4% -84.80% (p=0.000 n=10+10)\nRedisAddPresence-8 52.3\xb5s \xb1 4% 5.8\xb5s \xb1 2% -88.94% (p=0.000 n=10+10)\n\n(rest is not important here...)\n"})}),"\n",(0,t.jsxs)(n.p,{children:["We see that new Engine implementation behaves much better for most cases. But what happened to ",(0,t.jsx)(n.code,{children:"Subscribe"})," operation? It did not change at all in Redigo case \u2013 the same performance as if there is no additional latency involved!"]}),"\n",(0,t.jsxs)(n.p,{children:["Turned out that when we call ",(0,t.jsx)(n.code,{children:"Subscribe"})," in Redigo case, Redigo only flushes data to the network without waiting synchronously for subscribe result."]}),"\n",(0,t.jsx)(n.p,{children:"It makes sense in general and we can listen to subscribe notifications asynchronously, but in Centrifugo we relied on the returned error thinking that it includes succesful subscription result from Redis - meaning that we already subscribed to a channel at that point. And this could theoretically lead to some rare bugs in Centrifugo."}),"\n",(0,t.jsxs)(n.p,{children:["Rueidis library waits for subscribe response. So here the behavior of ",(0,t.jsx)(n.code,{children:"rueidis"})," while differs from ",(0,t.jsx)(n.code,{children:"redigo"})," in terms of throughput under increased latency just fits Centrifugo better in terms of behavior. So we go with it."]}),"\n",(0,t.jsx)(n.h2,{id:"conclusion",children:"Conclusion"}),"\n",(0,t.jsx)(n.p,{children:"Migrating from Redigo to Rueidis library was not just a task of rewriting code, we had to carefully test various aspects of Redis Engine behaviour \u2013 latency, throughput, CPU utilization of application, and even CPU utilization of Redis itself under the equal application load conditions."}),"\n",(0,t.jsxs)(n.p,{children:["I think that we will find more projects in Go ecosystem using ",(0,t.jsx)(n.code,{children:"rueidis"})," library shortly. Not just because of its allocation efficiency and out-of-the-box throughput, but also due to a convenient type-safe command API."]}),"\n",(0,t.jsx)(n.p,{children:"For most Centrifugo users this migration means more efficient CPU usage as new implementation allocates less memory (less work to allocate and less strain on GC) and we tried to find a reasonable batch size to reduce the number of system calls for common operations. While latency and throughput of single Centrifugo node should be better as we make concurrent Redis calls from many goroutines."}),"\n",(0,t.jsx)(n.p,{children:"Hopefully readers will learn some tips from this post which can help to achieve effective communication with Redis from Go or another programming language."}),"\n",(0,t.jsx)(n.p,{children:"A few key takeaways:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Redis pipelining may increase throughput and reduce latency, it can also reduce CPU utilization of Redis"}),"\n",(0,t.jsx)(n.li,{children:"Don't blindly trust Go benchmark numbers but also think about CPU effect of changes you made (sometimes of the external system also)"}),"\n",(0,t.jsx)(n.li,{children:"Reduce the number of system calls to decrease CPU utilization"}),"\n",(0,t.jsx)(n.li,{children:"Everything is a trade-off \u2013 latency or resource usage? Your own WebSocket server or Centrifugo?"}),"\n",(0,t.jsxs)(n.li,{children:["Don't rely on someone's else benchmarks, including those published here. ",(0,t.jsx)(n.strong,{children:"Measure for your own use case"}),". Take into account your load profile, paralellism, network latency, data size, etc."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["P.S. One thing worth mentioning and which may be helpful for someone is that during our comparison experiments we discovered that Redis 7 has a major latency increase compared to Redis 6 when executing Lua scripts. So if you have performance sensitive code with Lua scripts take a look at ",(0,t.jsx)(n.a,{href:"https://github.com/redis/redis/issues/10981",children:"this Redis issue"}),". With the help of Redis developers some things already improved in ",(0,t.jsx)(n.code,{children:"unstable"})," Redis branch, hopefully that issue will be closed at the time you read this post."]})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},29399:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/goredis_allocs-3b6ad3ecb2543d679e812619438962c3.png"},78811:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_cover-94601b44109b725188bcf75df0fb520c.png"},35577:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_pipeline-7fdadb07739a87c92e520d0fba06696d.png"},64169:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_vis01-8364cd7474502a278b9362d5f5e71245.png"},88999:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/redis_vis02-a8e078e75bac446b0c907beeeaf368ae.png"},6101:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/rueidis_1-05b9a03175ec64b4ec70679313bd4a38.png"},88457:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/rueidis_2-55b7b75cf531852fed8f73ceb7883b4d.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>a,a:()=>r});var t=i(67294);const s={},o=t.createContext(s);function r(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4ec37bcb.3b8ebc2a.js b/assets/js/4ec37bcb.3cc9f87e.js similarity index 93% rename from assets/js/4ec37bcb.3b8ebc2a.js rename to assets/js/4ec37bcb.3cc9f87e.js index debddce6c..5241b0120 100644 --- a/assets/js/4ec37bcb.3b8ebc2a.js +++ b/assets/js/4ec37bcb.3cc9f87e.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8899],{36549:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>s,metadata:()=>r,toc:()=>c});var i=o(85893),n=o(11151);const s={id:"layout",sidebar_label:"App layout and behavior",title:"App layout and behavior"},a=void 0,r={id:"tutorial/layout",title:"App layout and behavior",description:"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.",source:"@site/docs/tutorial/layout.md",sourceDirName:"tutorial",slug:"/tutorial/layout",permalink:"/docs/tutorial/layout",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/layout.md",tags:[],version:"current",frontMatter:{id:"layout",sidebar_label:"App layout and behavior",title:"App layout and behavior"},sidebar:"Tutorial",previous:{title:"Real-time app from scratch",permalink:"/docs/tutorial/intro"},next:{title:"Setting up backend and database",permalink:"/docs/tutorial/backend"}},l={},c=[{value:"App screens",id:"app-screens",level:2},{value:"Login Screen",id:"login-screen",level:3},{value:"Chat Room List Screen",id:"chat-room-list-screen",level:3},{value:"Chat Room Search Screen",id:"chat-room-search-screen",level:3},{value:"Chat Room Detail Screen",id:"chat-room-detail-screen",level:3},{value:"2-column layout in mind",id:"2-column-layout-in-mind",level:2}];function h(e){const t={del:"del",h2:"h2",h3:"h3",img:"img",p:"p",...(0,n.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.h2,{id:"app-screens",children:"App screens"}),"\n",(0,i.jsx)(t.p,{children:"We tried to find a good balance which screens to include into the app. The goal was to keep the result minimal while still showcasing the ideas covered in the tutorial. Our completed app consists of four screens."}),"\n",(0,i.jsx)(t.h3,{id:"login-screen",children:"Login Screen"}),"\n",(0,i.jsx)(t.p,{children:"One of the goals for the tutorial is showing the app with user authentication. To show how to tell Centrifugo which user connects and build permissions for channels around the particular user."}),"\n",(0,i.jsxs)(t.p,{children:["Nothing too special here \u2013 we will use native Django user/password authentication. Django already has built-in User model and functions to support user login/logout workflow. So we just use this.\n",(0,i.jsx)(t.img,{src:o(54853).Z+"",width:"2098",height:"1554"}),"\nThis screen does not include any real-time features. As soon as user logs into the app with its username/password pair we see the Chat Room List Screen."]}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-list-screen",children:"Chat Room List Screen"}),"\n",(0,i.jsxs)(t.p,{children:["This one shows rooms current user joined. So user can click on any to go to Chat Room Detail Screen.\n",(0,i.jsx)(t.img,{src:o(95141).Z+"",width:"2098",height:"1554"}),"\nThis screen includes a couple of elements to emphasize. First one is a green circle on top right. This is a Centrifugo real-time subscription status. As soon as user connected to Centrifugo and subscribed to the personal message stream (personal channel) - the indicator is green \ud83d\udfe2. Otherwise - it's red \ud83d\udd34."]}),"\n",(0,i.jsxs)(t.p,{children:["The next element - the number of ",(0,i.jsx)(t.del,{children:"cats"})," users who joined the specific room. This counter is synchronized in real-time. For example, if someone joins the room from Chat Room Search Screen (at which we will look shortly) \u2013 the counter will be instantly synchronized. We also automatically add/remove rooms if current user joins/leaves some room from within Chat Room Search Screen opened in another browser tab or another device."]}),"\n",(0,i.jsx)(t.p,{children:"For every room we show the beginning of the last message sent to the room. Upon receiving real-time message we re-order rooms to put the one with latest message on top."}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-search-screen",children:"Chat Room Search Screen"}),"\n",(0,i.jsxs)(t.p,{children:["This screen allows the user to discover new rooms to join. In our app we decided to not provide a functionality for the user to create chat rooms. Rooms must be pre-created by admin \u2013 it's actually possible to do using Django built-in admin web UI - so to keep tutorial shorter (not the ideal justification for this tutorial which is freaking large) we decided to skip it for now.\n",(0,i.jsx)(t.img,{src:o(15919).Z+"",width:"2098",height:"1554"}),"\nWe distinguish rooms current user joined or not joined by using color scheme. The information about current user membership is synchronized between browser tabs and different devices. After user joins the room \u2013 it appears on Chat Room List Screen on every user's device."]}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-detail-screen",children:"Chat Room Detail Screen"}),"\n",(0,i.jsxs)(t.p,{children:["Finally, a page with room name, list of messages and a possibility to send a new one:\n",(0,i.jsx)(t.img,{src:o(96529).Z+"",width:"2098",height:"1554"}),"\nOf course messages are sent in real-time to all users participating in chat. Also, the counter with number of users right to the room name is also updated in real-time."]}),"\n",(0,i.jsx)(t.h2,{id:"2-column-layout-in-mind",children:"2-column layout in mind"}),"\n",(0,i.jsx)(t.p,{children:"Often in messenger apps you can see the layout where a list of chats is the left column, and chat details shown on the right. Like this one in Slack:"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{src:"https://www.cnet.com/a/img/hub/2023/08/08/f4e09832-9f2b-4967-ac66-53fc8dfc6588/slack-redesign-2023-before-home.png",alt:"TBD"})}),"\n",(0,i.jsx)(t.p,{children:"While we use a slightly simplified layout in the app with a separate chat room list and chat detail screens (more often seen on mobile devices), we keep in mind the possibility to switch to the 2-column layout if needed - just with a change of React component arrangement and some CSS. With our implementation user may be theoretically a member of hundreds or thousands of rooms and receive updates from all of them on one screen. Like in Telegram, Discord or Slack messengers."}),"\n",(0,i.jsx)(t.p,{children:"This predetermined the fact we are using individual user channels in the app to receive real-time updates from all the rooms, instead of subscribing to each individual chat room channel. We will talk about this decision later, for now let's say simply: using individual real-time channels drastically simplifies frontend implementation, leaving the complexity for the backend side."})]})}function d(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},54853:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-login-47d4c7601d8afc0dd410bad433782350.png"},96529:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-room-detail-e730c799ba078cd94edd8914f4073b22.png"},95141:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-room-list-f08bcbf77fd6862c2b572fe906cac3f5.png"},15919:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-search-e4d2b0e435cedbc7e74a051095740b8e.png"},11151:(e,t,o)=>{o.d(t,{Z:()=>r,a:()=>a});var i=o(67294);const n={},s=i.createContext(n);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8899],{36549:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>s,metadata:()=>r,toc:()=>c});var i=o(85893),n=o(11151);const s={id:"layout",sidebar_label:"App layout and behavior",title:"App layout and behavior"},a=void 0,r={id:"tutorial/layout",title:"App layout and behavior",description:"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.",source:"@site/docs/tutorial/layout.md",sourceDirName:"tutorial",slug:"/tutorial/layout",permalink:"/docs/tutorial/layout",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/tutorial/layout.md",tags:[],version:"current",frontMatter:{id:"layout",sidebar_label:"App layout and behavior",title:"App layout and behavior"},sidebar:"Tutorial",previous:{title:"Real-time app from scratch",permalink:"/docs/tutorial/intro"},next:{title:"Setting up backend and database",permalink:"/docs/tutorial/backend"}},l={},c=[{value:"App screens",id:"app-screens",level:2},{value:"Login Screen",id:"login-screen",level:3},{value:"Chat Room List Screen",id:"chat-room-list-screen",level:3},{value:"Chat Room Search Screen",id:"chat-room-search-screen",level:3},{value:"Chat Room Detail Screen",id:"chat-room-detail-screen",level:3},{value:"2-column layout in mind",id:"2-column-layout-in-mind",level:2}];function h(e){const t={del:"del",h2:"h2",h3:"h3",img:"img",p:"p",...(0,n.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.p,{children:"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."}),"\n",(0,i.jsx)(t.h2,{id:"app-screens",children:"App screens"}),"\n",(0,i.jsx)(t.p,{children:"We tried to find a good balance which screens to include into the app. The goal was to keep the result minimal while still showcasing the ideas covered in the tutorial. Our completed app consists of four screens."}),"\n",(0,i.jsx)(t.h3,{id:"login-screen",children:"Login Screen"}),"\n",(0,i.jsx)(t.p,{children:"One of the goals for the tutorial is showing the app with user authentication. To show how to tell Centrifugo which user connects and build permissions for channels around the particular user."}),"\n",(0,i.jsxs)(t.p,{children:["Nothing too special here \u2013 we will use native Django user/password authentication. Django already has built-in User model and functions to support user login/logout workflow. So we just use this.\n",(0,i.jsx)(t.img,{src:o(77651).Z+"",width:"2098",height:"1554"}),"\nThis screen does not include any real-time features. As soon as user logs into the app with its username/password pair we see the Chat Room List Screen."]}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-list-screen",children:"Chat Room List Screen"}),"\n",(0,i.jsxs)(t.p,{children:["This one shows rooms current user joined. So user can click on any to go to Chat Room Detail Screen.\n",(0,i.jsx)(t.img,{src:o(56464).Z+"",width:"2098",height:"1554"}),"\nThis screen includes a couple of elements to emphasize. First one is a green circle on top right. This is a Centrifugo real-time subscription status. As soon as user connected to Centrifugo and subscribed to the personal message stream (personal channel) - the indicator is green \ud83d\udfe2. Otherwise - it's red \ud83d\udd34."]}),"\n",(0,i.jsxs)(t.p,{children:["The next element - the number of ",(0,i.jsx)(t.del,{children:"cats"})," users who joined the specific room. This counter is synchronized in real-time. For example, if someone joins the room from Chat Room Search Screen (at which we will look shortly) \u2013 the counter will be instantly synchronized. We also automatically add/remove rooms if current user joins/leaves some room from within Chat Room Search Screen opened in another browser tab or another device."]}),"\n",(0,i.jsx)(t.p,{children:"For every room we show the beginning of the last message sent to the room. Upon receiving real-time message we re-order rooms to put the one with latest message on top."}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-search-screen",children:"Chat Room Search Screen"}),"\n",(0,i.jsxs)(t.p,{children:["This screen allows the user to discover new rooms to join. In our app we decided to not provide a functionality for the user to create chat rooms. Rooms must be pre-created by admin \u2013 it's actually possible to do using Django built-in admin web UI - so to keep tutorial shorter (not the ideal justification for this tutorial which is freaking large) we decided to skip it for now.\n",(0,i.jsx)(t.img,{src:o(27830).Z+"",width:"2098",height:"1554"}),"\nWe distinguish rooms current user joined or not joined by using color scheme. The information about current user membership is synchronized between browser tabs and different devices. After user joins the room \u2013 it appears on Chat Room List Screen on every user's device."]}),"\n",(0,i.jsx)(t.h3,{id:"chat-room-detail-screen",children:"Chat Room Detail Screen"}),"\n",(0,i.jsxs)(t.p,{children:["Finally, a page with room name, list of messages and a possibility to send a new one:\n",(0,i.jsx)(t.img,{src:o(98717).Z+"",width:"2098",height:"1554"}),"\nOf course messages are sent in real-time to all users participating in chat. Also, the counter with number of users right to the room name is also updated in real-time."]}),"\n",(0,i.jsx)(t.h2,{id:"2-column-layout-in-mind",children:"2-column layout in mind"}),"\n",(0,i.jsx)(t.p,{children:"Often in messenger apps you can see the layout where a list of chats is the left column, and chat details shown on the right. Like this one in Slack:"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{src:"https://www.cnet.com/a/img/hub/2023/08/08/f4e09832-9f2b-4967-ac66-53fc8dfc6588/slack-redesign-2023-before-home.png",alt:"TBD"})}),"\n",(0,i.jsx)(t.p,{children:"While we use a slightly simplified layout in the app with a separate chat room list and chat detail screens (more often seen on mobile devices), we keep in mind the possibility to switch to the 2-column layout if needed - just with a change of React component arrangement and some CSS. With our implementation user may be theoretically a member of hundreds or thousands of rooms and receive updates from all of them on one screen. Like in Telegram, Discord or Slack messengers."}),"\n",(0,i.jsx)(t.p,{children:"This predetermined the fact we are using individual user channels in the app to receive real-time updates from all the rooms, instead of subscribing to each individual chat room channel. We will talk about this decision later, for now let's say simply: using individual real-time channels drastically simplifies frontend implementation, leaving the complexity for the backend side."})]})}function d(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},77651:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-login-47d4c7601d8afc0dd410bad433782350.png"},98717:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-room-detail-e730c799ba078cd94edd8914f4073b22.png"},56464:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-room-list-f08bcbf77fd6862c2b572fe906cac3f5.png"},27830:(e,t,o)=>{o.d(t,{Z:()=>i});const i=o.p+"assets/images/grand-chat-search-e4d2b0e435cedbc7e74a051095740b8e.png"},11151:(e,t,o)=>{o.d(t,{Z:()=>r,a:()=>a});var i=o(67294);const n={},s=i.createContext(n);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/5072.1c9b2ea9.js b/assets/js/5072.1c9b2ea9.js deleted file mode 100644 index 81504e698..000000000 --- a/assets/js/5072.1c9b2ea9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5072],{89244:(e,t,n)=>{n.d(t,{Z:()=>a});n(67294);var i=n(36905),o=n(11614),s=n(34055),r=n(85893);function a(e){let{className:t}=e;return(0,r.jsx)("main",{className:(0,i.Z)("container margin-vert--xl",t),children:(0,r.jsx)("div",{className:"row",children:(0,r.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,r.jsx)(s.Z,{as:"h1",className:"hero__title",children:(0,r.jsx)(o.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,r.jsx)("p",{children:(0,r.jsx)(o.Z,{id:"theme.NotFound.p1",description:"The first paragraph of the 404 page",children:"We could not find what you were looking for."})}),(0,r.jsx)("p",{children:(0,r.jsx)(o.Z,{id:"theme.NotFound.p2",description:"The 2nd paragraph of the 404 page",children:"Please contact the owner of the site that linked you to the original URL and let them know their link is broken."})})]})})})}},25072:(e,t,n)=>{n.r(t),n.d(t,{default:()=>l});n(67294);var i=n(11614),o=n(62581),s=n(78299),r=n(89244),a=n(85893);function l(){const e=(0,i.I)({id:"theme.NotFound.title",message:"Page Not Found"});return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(o.d,{title:e}),(0,a.jsx)(s.Z,{children:(0,a.jsx)(r.Z,{})})]})}}}]); \ No newline at end of file diff --git a/assets/js/5386.d06b5c39.js b/assets/js/5386.d06b5c39.js new file mode 100644 index 000000000..df5ba4044 --- /dev/null +++ b/assets/js/5386.d06b5c39.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5386],{59047:(e,n,t)=>{t.d(n,{Z:()=>T});var i=t(67294),s=t(85893);function a(e){const{mdxAdmonitionTitle:n,rest:t}=function(e){const n=i.Children.toArray(e),t=n.find((e=>i.isValidElement(e)&&"mdxAdmonitionTitle"===e.type)),a=n.filter((e=>e!==t)),l=t?.props.children;return{mdxAdmonitionTitle:l,rest:a.length>0?(0,s.jsx)(s.Fragment,{children:a}):null}}(e.children),a=e.title??n;return{...e,...a&&{title:a},children:t}}var l=t(36905),r=t(95999),o=t(35281);const c={admonition:"admonition_xJq3",admonitionHeading:"admonitionHeading_Gvgb",admonitionIcon:"admonitionIcon_Rf37",admonitionContent:"admonitionContent_BuS1"};function d(e){let{type:n,className:t,children:i}=e;return(0,s.jsx)("div",{className:(0,l.Z)(o.k.common.admonition,o.k.common.admonitionType(n),c.admonition,t),children:i})}function u(e){let{icon:n,title:t}=e;return(0,s.jsxs)("div",{className:c.admonitionHeading,children:[(0,s.jsx)("span",{className:c.admonitionIcon,children:n}),t]})}function m(e){let{children:n}=e;return n?(0,s.jsx)("div",{className:c.admonitionContent,children:n}):null}function h(e){const{type:n,icon:t,title:i,children:a,className:l}=e;return(0,s.jsxs)(d,{type:n,className:l,children:[i||t?(0,s.jsx)(u,{title:i,icon:t}):null,(0,s.jsx)(m,{children:a})]})}function f(e){return(0,s.jsx)("svg",{viewBox:"0 0 14 16",...e,children:(0,s.jsx)("path",{fillRule:"evenodd",d:"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"})})}const x={icon:(0,s.jsx)(f,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.note",description:"The default label used for the Note admonition (:::note)",children:"note"})};function v(e){return(0,s.jsx)(h,{...x,...e,className:(0,l.Z)("alert alert--secondary",e.className),children:e.children})}function p(e){return(0,s.jsx)("svg",{viewBox:"0 0 12 16",...e,children:(0,s.jsx)("path",{fillRule:"evenodd",d:"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"})})}const j={icon:(0,s.jsx)(p,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function g(e){return(0,s.jsx)(h,{...j,...e,className:(0,l.Z)("alert alert--success",e.className),children:e.children})}function N(e){return(0,s.jsx)("svg",{viewBox:"0 0 14 16",...e,children:(0,s.jsx)("path",{fillRule:"evenodd",d:"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"})})}const b={icon:(0,s.jsx)(N,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.info",description:"The default label used for the Info admonition (:::info)",children:"info"})};function Z(e){return(0,s.jsx)(h,{...b,...e,className:(0,l.Z)("alert alert--info",e.className),children:e.children})}function C(e){return(0,s.jsx)("svg",{viewBox:"0 0 16 16",...e,children:(0,s.jsx)("path",{fillRule:"evenodd",d:"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"})})}const y={icon:(0,s.jsx)(C,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function L(e){return(0,s.jsx)("svg",{viewBox:"0 0 12 16",...e,children:(0,s.jsx)("path",{fillRule:"evenodd",d:"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"})})}const H={icon:(0,s.jsx)(L,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const w={icon:(0,s.jsx)(C,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const k={...{note:v,tip:g,info:Z,warning:function(e){return(0,s.jsx)(h,{...y,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,s.jsx)(h,{...H,...e,className:(0,l.Z)("alert alert--danger",e.className),children:e.children})}},...{secondary:e=>(0,s.jsx)(v,{title:"secondary",...e}),important:e=>(0,s.jsx)(Z,{title:"important",...e}),success:e=>(0,s.jsx)(g,{title:"success",...e}),caution:function(e){return(0,s.jsx)(h,{...w,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})}}};function T(e){const n=a(e),t=(i=n.type,k[i]||(console.warn(`No admonition component found for admonition type "${i}". Using Info as fallback.`),k.info));var i;return(0,s.jsx)(t,{...n})}},47265:(e,n,t)=>{t.d(n,{Z:()=>v});t(67294);var i=t(36905),s=t(95999),a=t(35281),l=t(33692);const r={iconEdit:"iconEdit_Z9Sw"};var o=t(85893);function c(e){let{className:n,...t}=e;return(0,o.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,i.Z)(r.iconEdit,n),"aria-hidden":"true",...t,children:(0,o.jsx)("g",{children:(0,o.jsx)("path",{d:"m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z"})})})}function d(e){let{editUrl:n}=e;return(0,o.jsxs)(l.Z,{to:n,className:a.k.common.editThisPage,children:[(0,o.jsx)(c,{}),(0,o.jsx)(s.Z,{id:"theme.common.editThisPage",description:"The link label to edit the current page",children:"Edit this page"})]})}var u=t(9788);function m(e){let{lastUpdatedAt:n}=e;const t=new Date(n),i=(0,u.P)({day:"numeric",month:"short",year:"numeric",timeZone:"UTC"}).format(t);return(0,o.jsx)(s.Z,{id:"theme.lastUpdated.atDate",description:"The words used to describe on which date a page has been last updated",values:{date:(0,o.jsx)("b",{children:(0,o.jsx)("time",{dateTime:t.toISOString(),itemProp:"dateModified",children:i})})},children:" on {date}"})}function h(e){let{lastUpdatedBy:n}=e;return(0,o.jsx)(s.Z,{id:"theme.lastUpdated.byUser",description:"The words used to describe by who the page has been last updated",values:{user:(0,o.jsx)("b",{children:n})},children:" by {user}"})}function f(e){let{lastUpdatedAt:n,lastUpdatedBy:t}=e;return(0,o.jsxs)("span",{className:a.k.common.lastUpdated,children:[(0,o.jsx)(s.Z,{id:"theme.lastUpdated.lastUpdatedAtBy",description:"The sentence used to display when a page has been last updated, and by who",values:{atDate:n?(0,o.jsx)(m,{lastUpdatedAt:n}):"",byUser:t?(0,o.jsx)(h,{lastUpdatedBy:t}):""},children:"Last updated{atDate}{byUser}"}),!1]})}const x={lastUpdated:"lastUpdated_JAkA"};function v(e){let{className:n,editUrl:t,lastUpdatedAt:s,lastUpdatedBy:a}=e;return(0,o.jsxs)("div",{className:(0,i.Z)("row",n),children:[(0,o.jsx)("div",{className:"col",children:t&&(0,o.jsx)(d,{editUrl:t})}),(0,o.jsx)("div",{className:(0,i.Z)("col",x.lastUpdated),children:(s||a)&&(0,o.jsx)(f,{lastUpdatedAt:s,lastUpdatedBy:a})})]})}},95896:(e,n,t)=>{t.d(n,{Z:()=>U});var i=t(67294),s=t(11151),a=t(35742),l=t(9286),r=t(85893);function o(e){return(0,r.jsx)("code",{...e})}var c=t(33692);var d=t(36905),u=t(788),m=t(28138),h=t(72389),f=t(86043);const x={details:"details_lb9f",isBrowser:"isBrowser_bmU9",collapsibleContent:"collapsibleContent_i85q"};function v(e){return!!e&&("SUMMARY"===e.tagName||v(e.parentElement))}function p(e,n){return!!e&&(e===n||p(e.parentElement,n))}function j(e){let{summary:n,children:t,...s}=e;(0,m.Z)().collectAnchor(s.id);const a=(0,h.Z)(),l=(0,i.useRef)(null),{collapsed:o,setCollapsed:c}=(0,f.u)({initialState:!s.open}),[d,j]=(0,i.useState)(s.open),g=i.isValidElement(n)?n:(0,r.jsx)("summary",{children:n??"Details"});return(0,r.jsxs)("details",{...s,ref:l,open:d,"data-collapsed":o,className:(0,u.Z)(x.details,a&&x.isBrowser,s.className),onMouseDown:e=>{v(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();const n=e.target;v(n)&&p(n,l.current)&&(e.preventDefault(),o?(c(!1),j(!0)):c(!0))},children:[g,(0,r.jsx)(f.z,{lazy:!1,collapsed:o,disableSSRStyle:!0,onCollapseTransitionEnd:e=>{c(e),j(!e)},children:(0,r.jsx)("div",{className:x.collapsibleContent,children:t})})]})}const g={details:"details_b_Ee"},N="alert alert--info";function b(e){let{...n}=e;return(0,r.jsx)(j,{...n,className:(0,d.Z)(N,g.details,n.className)})}function Z(e){const n=i.Children.toArray(e.children),t=n.find((e=>i.isValidElement(e)&&"summary"===e.type)),s=(0,r.jsx)(r.Fragment,{children:n.filter((e=>e!==t))});return(0,r.jsx)(b,{...e,summary:t,children:s})}var C=t(92503);function y(e){return(0,r.jsx)(C.Z,{...e})}const L={containsTaskList:"containsTaskList_mC6p"};function H(e){if(void 0!==e)return(0,d.Z)(e,e?.includes("contains-task-list")&&L.containsTaskList)}const w={img:"img_ev3q"};var k=t(59047);const T={Head:a.Z,details:Z,Details:Z,code:function(e){return function(e){return void 0!==e.children&&i.Children.toArray(e.children).every((e=>"string"==typeof e&&!e.includes("\n")))}(e)?(0,r.jsx)(o,{...e}):(0,r.jsx)(l.Z,{...e})},a:function(e){return(0,r.jsx)(c.Z,{...e})},pre:function(e){return(0,r.jsx)(r.Fragment,{children:e.children})},ul:function(e){return(0,r.jsx)("ul",{...e,className:H(e.className)})},li:function(e){return(0,m.Z)().collectAnchor(e.id),(0,r.jsx)("li",{...e})},img:function(e){return(0,r.jsx)("img",{decoding:"async",loading:"lazy",...e,className:(n=e.className,(0,d.Z)(n,w.img))});var n},h1:e=>(0,r.jsx)(y,{as:"h1",...e}),h2:e=>(0,r.jsx)(y,{as:"h2",...e}),h3:e=>(0,r.jsx)(y,{as:"h3",...e}),h4:e=>(0,r.jsx)(y,{as:"h4",...e}),h5:e=>(0,r.jsx)(y,{as:"h5",...e}),h6:e=>(0,r.jsx)(y,{as:"h6",...e}),admonition:k.Z,mermaid:()=>null};function U(e){let{children:n}=e;return(0,r.jsx)(s.Z,{components:T,children:n})}},39407:(e,n,t)=>{t.d(n,{Z:()=>c});t(67294);var i=t(36905),s=t(93743);const a={tableOfContents:"tableOfContents_bqdL",docItemContainer:"docItemContainer_F8PC"};var l=t(85893);const r="table-of-contents__link toc-highlight",o="table-of-contents__link--active";function c(e){let{className:n,...t}=e;return(0,l.jsx)("div",{className:(0,i.Z)(a.tableOfContents,"thin-scrollbar",n),children:(0,l.jsx)(s.Z,{...t,linkClassName:r,linkActiveClassName:o})})}},93743:(e,n,t)=>{t.d(n,{Z:()=>x});var i=t(67294),s=t(86668);function a(e){const n=e.map((e=>({...e,parentIndex:-1,children:[]}))),t=Array(7).fill(-1);n.forEach(((e,n)=>{const i=t.slice(2,e.level);e.parentIndex=Math.max(...i),t[e.level]=n}));const i=[];return n.forEach((e=>{const{parentIndex:t,...s}=e;t>=0?n[t].children.push(s):i.push(s)})),i}function l(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:i}=e;return n.flatMap((e=>{const n=l({toc:e.children,minHeadingLevel:t,maxHeadingLevel:i});return function(e){return e.level>=t&&e.level<=i}(e)?[{...e,children:n}]:n}))}function r(e){const n=e.getBoundingClientRect();return n.top===n.bottom?r(e.parentNode):n}function o(e,n){let{anchorTopOffset:t}=n;const i=e.find((e=>r(e).top>=t));if(i){return function(e){return e.top>0&&e.bottom<window.innerHeight/2}(r(i))?i:e[e.indexOf(i)-1]??null}return e[e.length-1]??null}function c(){const e=(0,i.useRef)(0),{navbar:{hideOnScroll:n}}=(0,s.L)();return(0,i.useEffect)((()=>{e.current=n?0:document.querySelector(".navbar").clientHeight}),[n]),e}function d(e){const n=(0,i.useRef)(void 0),t=c();(0,i.useEffect)((()=>{if(!e)return()=>{};const{linkClassName:i,linkActiveClassName:s,minHeadingLevel:a,maxHeadingLevel:l}=e;function r(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(i),r=function(e){let{minHeadingLevel:n,maxHeadingLevel:t}=e;const i=[];for(let s=n;s<=t;s+=1)i.push(`h${s}.anchor`);return Array.from(document.querySelectorAll(i.join()))}({minHeadingLevel:a,maxHeadingLevel:l}),c=o(r,{anchorTopOffset:t.current}),d=e.find((e=>c&&c.id===function(e){return decodeURIComponent(e.href.substring(e.href.indexOf("#")+1))}(e)));e.forEach((e=>{!function(e,t){t?(n.current&&n.current!==e&&n.current.classList.remove(s),e.classList.add(s),n.current=e):e.classList.remove(s)}(e,e===d)}))}return document.addEventListener("scroll",r),document.addEventListener("resize",r),r(),()=>{document.removeEventListener("scroll",r),document.removeEventListener("resize",r)}}),[e,t])}var u=t(33692),m=t(85893);function h(e){let{toc:n,className:t,linkClassName:i,isChild:s}=e;return n.length?(0,m.jsx)("ul",{className:s?void 0:t,children:n.map((e=>(0,m.jsxs)("li",{children:[(0,m.jsx)(u.Z,{to:`#${e.id}`,className:i??void 0,dangerouslySetInnerHTML:{__html:e.value}}),(0,m.jsx)(h,{isChild:!0,toc:e.children,className:t,linkClassName:i})]},e.id)))}):null}const f=i.memo(h);function x(e){let{toc:n,className:t="table-of-contents table-of-contents__left-border",linkClassName:r="table-of-contents__link",linkActiveClassName:o,minHeadingLevel:c,maxHeadingLevel:u,...h}=e;const x=(0,s.L)(),v=c??x.tableOfContents.minHeadingLevel,p=u??x.tableOfContents.maxHeadingLevel,j=function(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:s}=e;return(0,i.useMemo)((()=>l({toc:a(n),minHeadingLevel:t,maxHeadingLevel:s})),[n,t,s])}({toc:n,minHeadingLevel:v,maxHeadingLevel:p});return d((0,i.useMemo)((()=>{if(r&&o)return{linkClassName:r,linkActiveClassName:o,minHeadingLevel:v,maxHeadingLevel:p}}),[r,o,v,p])),(0,m.jsx)(f,{toc:j,className:t,linkClassName:r,...h})}},22212:(e,n,t)=>{t.d(n,{Z:()=>h});t(67294);var i=t(36905),s=t(95999),a=t(35742),l=t(85893);function r(){return(0,l.jsx)(s.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function o(){return(0,l.jsx)(s.Z,{id:"theme.unlistedContent.message",description:"The unlisted content banner message",children:"This page is unlisted. Search engines will not index it, and only users having a direct link can access it."})}function c(){return(0,l.jsx)(a.Z,{children:(0,l.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})}var d=t(35281),u=t(59047);function m(e){let{className:n}=e;return(0,l.jsx)(u.Z,{type:"caution",title:(0,l.jsx)(r,{}),className:(0,i.Z)(n,d.k.common.unlistedBanner),children:(0,l.jsx)(o,{})})}function h(e){return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c,{}),(0,l.jsx)(m,{...e})]})}},9788:(e,n,t)=>{t.d(n,{P:()=>s});var i=t(52263);function s(e){void 0===e&&(e={});const{i18n:{currentLocale:n}}=(0,i.Z)(),t=function(){const{i18n:{currentLocale:e,localeConfigs:n}}=(0,i.Z)();return n[e].calendar}();return new Intl.DateTimeFormat(n,{calendar:t,...e})}}}]); \ No newline at end of file diff --git a/assets/js/56231886.0d3d77fe.js b/assets/js/56231886.43d386d4.js similarity index 96% rename from assets/js/56231886.0d3d77fe.js rename to assets/js/56231886.43d386d4.js index ecaff30fd..924aefa85 100644 --- a/assets/js/56231886.0d3d77fe.js +++ b/assets/js/56231886.43d386d4.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5407],{34037:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>r,toc:()=>l});var i=t(85893),s=t(11151);const a={id:"quickstart",sidebar_label:"Quickstart tutorial",title:"Quickstart tutorial \u23f1\ufe0f"},o=void 0,r={id:"getting-started/quickstart",title:"Quickstart tutorial \u23f1\ufe0f",description:"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.",source:"@site/versioned_docs/version-3/getting-started/quickstart.md",sourceDirName:"getting-started",slug:"/getting-started/quickstart",permalink:"/docs/3/getting-started/quickstart",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-3/getting-started/quickstart.md",tags:[],version:"3",frontMatter:{id:"quickstart",sidebar_label:"Quickstart tutorial",title:"Quickstart tutorial \u23f1\ufe0f"},sidebar:"Introduction",previous:{title:"Install Centrifugo",permalink:"/docs/3/getting-started/installation"},next:{title:"Main highlights",permalink:"/docs/3/getting-started/highlights"}},c={},l=[{value:"More examples",id:"more-examples",level:3}];function d(e){const n={a:"a",code:"code",h3:"h3",img:"img",p:"p",pre:"pre",strong:"strong",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"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."}),"\n",(0,i.jsxs)(n.p,{children:["First you need to ",(0,i.jsx)(n.a,{href:"/docs/3/getting-started/installation",children:"install Centrifugo"}),". Below in this example, we will use a binary file release for simplicity. Once you have Centrifugo available on your machine you can generate minimal required configuration file with the following command:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"./centrifugo genconfig\n"})}),"\n",(0,i.jsxs)(n.p,{children:["This helper command will generate ",(0,i.jsx)(n.code,{children:"config.json"})," file in the working directory with content like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133",\n "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b",\n "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401",\n "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6",\n "allowed_origins": []\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Now we can start a server. Let's start it with a built-in admin web interface:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-console",children:"./centrifugo --config=config.json --admin\n"})}),"\n",(0,i.jsxs)(n.p,{children:["We could also enable the admin web interface by not using ",(0,i.jsx)(n.code,{children:"--admin"})," flag but by adding ",(0,i.jsx)(n.code,{children:'"admin": true'})," option to the JSON configuration file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_hmac_secret_key": "46b38493-147e-4e3f-86e0-dc5ec54f5133",\n "admin": true,\n "admin_password": "ad0dff75-3131-4a02-8d64-9279b4f1c57b",\n "admin_secret": "583bc4b7-0fa5-4c4a-8566-16d3ce4ad401",\n "api_key": "aaaf202f-b5f8-4b34-bf88-f6c03a1ecda6",\n "allowed_origins": []\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"And then running only with a path to a configuration file:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-console",children:"./centrifugo --config=config.json\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Now open ",(0,i.jsx)(n.a,{href:"http://localhost:8000",children:"http://localhost:8000"}),". You should see Centrifugo admin web panel. Enter ",(0,i.jsx)(n.code,{children:"admin_password"})," value from the configuration file to log in."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Admin web panel",src:t(49260).Z+"",width:"2896",height:"1448"})}),"\n",(0,i.jsx)(n.p,{children:"Inside the admin panel, you should see that one Centrifugo node is running, and it does not have connected clients."}),"\n",(0,i.jsxs)(n.p,{children:["Now let's create ",(0,i.jsx)(n.code,{children:"index.html"})," file with our simple app:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-html",metastring:'title="index.html"',children:'<html>\n <head>\n <title>Centrifugo quick start\n \n \n
-
\n - + +
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
diff --git a/blog/2020/02/10/million-connections-with-centrifugo.html b/blog/2020/02/10/million-connections-with-centrifugo.html index 383673097..2847ea6f0 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 @@ - - + +

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 687dfe53a..0bfae8056 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 641597104..75f5a8b99 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 0e06e5f97..d3f71161a 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 c1a2324fb..a89965d31 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 34fb92839..9e78450bc 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 4e85d3914..51c678063 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 f04cced9c..5a700d453 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 5b453866a..f2b81e4fc 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 8720b6710..8d07055df 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 11c200cca..73698e6c6 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 0b086ca24..3e6a70e1f 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 de9e1f513..2207ef373 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 0940b09cd..a605f77fd 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 c72370bc1..90874d936 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 9c8d44535..e71733691 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 949b6b615..66909c884 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 73d1acaa5..d35f96356 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
diff --git a/blog/2024/06/03/real-time-document-state-sync.html b/blog/2024/06/03/real-time-document-state-sync.html index d88e39e79..15e98ee5c 100644 --- a/blog/2024/06/03/real-time-document-state-sync.html +++ b/blog/2024/06/03/real-time-document-state-sync.html @@ -15,8 +15,8 @@ - - + +

Proper real-time document state synchronization within Centrifugal ecosystem

· 11 min read
Alexander Emelin
Alexander Emelin
Founder of Centrifugal Labs
diff --git a/blog/archive.html b/blog/archive.html index 4a19eacf4..33d01b47a 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -15,8 +15,8 @@ - - + +
diff --git a/blog/tags.html b/blog/tags.html index 5add1092b..3bc290f92 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -15,8 +15,8 @@ - - + + diff --git a/blog/tags/authentication.html b/blog/tags/authentication.html index a71e9a9f3..1c4b6794e 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 0a5c0c546..b18c076be 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 77ac7cc3b..6881ea15c 100644 --- a/blog/tags/centrifuge.html +++ b/blog/tags/centrifuge.html @@ -15,8 +15,8 @@ - - + +

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
diff --git a/blog/tags/centrifugo.html b/blog/tags/centrifugo.html index 921b7aaf7..e9cfaa620 100644 --- a/blog/tags/centrifugo.html +++ b/blog/tags/centrifugo.html @@ -15,8 +15,8 @@ - - + +

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.
diff --git a/blog/tags/compression.html b/blog/tags/compression.html index 3badb6ff6..affff05f2 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 0c14f5712..d0308ad3f 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 index ea46f9e25..618e00b30 100644 --- a/blog/tags/docsync.html +++ b/blog/tags/docsync.html @@ -15,8 +15,8 @@ - - + +

One post tagged with "docsync"

View All Tags
diff --git a/blog/tags/go.html b/blog/tags/go.html index 211e61593..9152c9fb2 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 2c1ee4c2c..239b4b612 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 0fa753d3c..ce7317f9b 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 fda9096c5..f03accc38 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 c88f241cd..dc928e13d 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 55c27310a..a6eeeab86 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 6d72d1148..d79c12fd9 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 a4865b0b4..228ef4eab 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 8b5edc2a3..072fae01f 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 88755ac01..0f83eb7b6 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 15933a7be..a8140a6ae 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 9444c3b6d..96ae6a22e 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 513629744..f950dc9d2 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 5ea584a9d..784dbb072 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 8186a04dd..b7111bb46 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 6157b90fa..a64916089 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 11f7cf38e..4db81fa38 100644 --- a/blog/tags/websocket.html +++ b/blog/tags/websocket.html @@ -15,8 +15,8 @@ - - + +

3 posts tagged with "websocket"

View All Tags
diff --git a/blog/tags/webtransport.html b/blog/tags/webtransport.html index 7a472b7e2..854e3c74a 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 018186538..5800a8cb3 100644 --- a/components/Highlight.html +++ b/components/Highlight.html @@ -15,8 +15,8 @@ - - + +

diff --git a/components/logo.html b/components/logo.html index 7248d06ec..7fdae9509 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 9c5520f8e..24afd1014 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 49c6ef3e9..fe24cbffe 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 d31b28744..00521a4dc 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 7d89196e8..b8e833c96 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 7e9d550a9..210cede4b 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 4124b34f2..9ef9963c9 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 a5a08dba4..0ea76ff0b 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 6868fb7af..38fa5560e 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 151e5a3a1..1f763e82c 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 770b2b01b..682cf5da4 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 6fdbf3021..9b6c89dc6 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 3e1c74657..b86c7aefb 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 74ba85498..3f7bef347 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 d04719e1f..15e2ecd53 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 e4c53000d..fa00def86 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 bc3266484..0bf534840 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 d593fcdb4..8c3efc0f5 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 735096262..d898a28e0 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 87ee22cce..2cffb0788 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 58b980ae2..9bba5ca81 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 31ee99faa..26b5e19b5 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 21b62a1af..090eebdb8 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 12cd26c04..e1cb604d0 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 86ec173b0..30d35de69 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 13665dd9b..04f725dab 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 ac53f68ef..8a2100988 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 8f08333f3..ac9135f7c 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 e741161ab..4bad708d6 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 b3a4f52e9..3e7f6b7dc 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 f7d5ccd2b..0a19a59e4 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 6eea6edbe..4f8c9f520 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 8bb8b52bf..2fad0d820 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 21fc756c1..b91dcd471 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 402ba538c..37af9e48e 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 b5c9034f1..83d2db407 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 5f30120ad..86b4ecab9 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 85e52304e..7afe91eb8 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 bb356a9b8..465728a9c 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 67dda63a2..a000c3695 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 f9a49c5ba..f59d2bd4b 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 1fb9a0d67..54e00c5ce 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 44adf39f1..c9c21cfce 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 198730a2d..dfd6a2777 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 82ef46d6f..2e4800d55 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 96532e35a..44a0d2bae 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 ba0aa2b56..6918293b6 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 bb085caec..c8e500a67 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 546953ba5..d8fc24dbf 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 035ee8165..858f32f36 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 864785f66..ebcb531af 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 ee9d3ced2..8b553a9ae 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 b9fe3f3dc..0e19b5345 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 7d6cf5778..e607c1733 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 7b1ec0238..f653f1d5f 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 329c1d88c..9f8c983f3 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 892d76f99..a378da173 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 13e1698ce..963fc8c9c 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 009acbdf2..30244ae8d 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 e51c116cc..971fb6869 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 79798fe85..37290ecd3 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 822c0454d..4494b254c 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 f2172017f..05eee4b14 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 6d6a01ae7..b1ecdd11f 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 2845234d0..7e3700189 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 5d1b8b4fc..f87a96a5e 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 f1afcd065..b0c6d50f5 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 f9b88130f..5b20973ff 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 5740ed0b3..fd34bfc36 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 c79ba10a6..1a588d6b7 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 48c8dff7f..6bbe12093 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 0617038f8..554c65b22 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 86751ede6..71922c21e 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 d6cf4e061..1546583aa 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 6bfe527ac..fdfc29bd2 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 3297c5753..de6f80001 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 aff2764ec..a85bd66fe 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 562702e5f..724cf0ccf 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 5d7bab0a4..71e802b58 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 8e4a2e835..4e98f1645 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 ceb7df7d2..f608b58c1 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 43608cd33..320f4a18e 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 94f49ea3a..c0e26d0b4 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 106650f14..4dbb4c2b6 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 2546bac24..78e3ab35a 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 71c129964..001ecbe6e 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 cd79b8274..81547eb97 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 f61a73bcc..da10500b8 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 e1cec6c62..19d5e38e0 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 7f722b603..c329d1b83 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 62fe14f3a..db37acd20 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 f250ac8f5..3920aa43b 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 89bfb891c..aed3c6c64 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 088e59bcd..74d13bd8d 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 353fea0f4..49963c166 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 1dcba0af4..330e64939 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 db7829c91..baf8b4106 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 ff7d5059f..303cd59bc 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 5c78cfa8b..872532714 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 4aee296c0..64f3b945d 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 c66fe8988..31e682734 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 7e960763e..da049c31f 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 fd3eaf348..01c3866a9 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 d60880fc9..3d2adafef 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 830ea6bdc..7dd8edfc0 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 f26ffe2f6..6a5503aec 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 658cdb5e1..60ac2dd8d 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 4304a87f0..88829b5c8 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 e05ef45ca..ba44675c4 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 40eb64fa6..49d8b377c 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 e5465c7a1..1491424c4 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 0b10ebdf4..e1132450e 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 a0f1f7d73..e676dab17 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 b4d961a31..9abe44f74 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 d52d86034..ef39a747e 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 93be9a69f..53bc84bab 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 59235a489..7d924a2fd 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 e0284c923..76fb387c0 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 2adf05479..4cfbf8763 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 1546dcfe9..322816b20 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 ee9f06c71..ee264963c 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 15a2b7499..78876c706 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 152a41c0f..36e8a6dab 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 913f609f3..f50b40af6 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 4f9668189..c5c10003e 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 1b934b4f9..146aad0d2 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 83099cf5b..5083ed21c 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 9486b9129..fd35a396f 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 df963c39d..e986eb1fc 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 84ed89b11..96a283ebf 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 692993459..a0a6ea854 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 19c10c9cc..6da3ae21e 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 9980afcc7..7a36c39bf 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 2411f87b7..dce9facf6 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 f0a522be9..1b2e2f29e 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 5e2e8c05b..7cec619f8 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 788ebc30a..7a72cc439 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 4e3c63a03..7d1bc5835 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 b2a2e5759..8e3acd967 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 bece532ee..151560902 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 6246c3156..d8a815e56 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 de0cf0f44..3c19f26fc 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 55fc45ae9..f69b95825 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 ea47793c0..2f6b6faf0 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.

@@ -32,10 +32,10 @@

Kubernetes<
values.yaml
...
image:
registry: docker.io
repository: centrifugo/centrifugo-pro
tag: v5.4.1

Debian and Ubuntu

DEB package available in release assets.

-
wget https://github.com/centrifugal/centrifugo-pro/releases/download/v5.4.1/centrifugo-pro_5.4.1_amd64.deb
sudo dpkg -i centrifugo-pro_5.4.1_amd64.deb
+
wget https://github.com/centrifugal/centrifugo-pro/releases/download/v5.4.1/centrifugo-pro_5.4.1-0_amd64.deb
sudo dpkg -i centrifugo-pro_5.4.1_amd64.deb

Centos

RPM package available in release assets.

-
wget https://github.com/centrifugal/centrifugo-pro/releases/download/v5.4.1/centrifugo-pro-5.4.1.x86_64.rpm
sudo yum install centrifugo-pro-5.4.1.x86_64.rpm
+
wget https://github.com/centrifugal/centrifugo-pro/releases/download/v5.4.1/centrifugo-pro-5.4.1-0.x86_64.rpm
sudo yum install centrifugo-pro-5.4.1.x86_64.rpm

Setting PRO license key

Centrifugo PRO inherits all features and configuration options from open-source version. The only difference is that it expects a valid license key on start to avoid sandbox mode limits.

Once you have installed a PRO version and have a license key you can set it in configuration over license field, or pass over environment variables as CENTRIFUGO_LICENSE. Like this:

diff --git a/docs/pro/observability_enhancements.html b/docs/pro/observability_enhancements.html index 27ef9dc7a..6dde4d9af 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 63e5e2141..b780fa5e3 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 ba3a0f9db..40128e071 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 f61150c26..3935c40b0 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 f682a074b..5beaaef4e 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 9ce50cb8a..2b8568c70 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 c16a7e5ed..fe23b46e7 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 3f2ca16a0..ddd6db3ad 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 49f809c69..062ae4d55 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 9a7766e6a..e9aa54545 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 302a58388..636c8dc6c 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 6c577cd0e..20b564d84 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 533460177..a6b9e302e 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 2902d31ed..31cfa3792 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 be26d1a6a..be5f871ab 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 2de25223f..7e69ad7c1 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 61d225933..6b235b334 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 b88646a3f..dc054e279 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 e7960e4e1..d0d50dfec 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 9e1550531..8eb64242b 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 1d456117f..5a3e92ff9 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 9b96c4e88..393540bce 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 1dabb4569..1b674eb56 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 8946e9393..f50c31c98 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 e0b12b824..dbd1972da 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 151baf9cd..f5d46bce9 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 3e67b039f..5298b3a25 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 c764a4f93..e48d3ff85 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 a71464286..ade618d85 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 f69b9426d..dc5a8806e 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 4de49f46f..1dbfb30a1 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 c4a585c3c..3a5dd46e6 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 70e0d39ba..00f77a752 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 896d62312..a34a4c0c7 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 53bb695a3..126db3b78 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 021236d9d..97b3f44d0 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 003c49996..8e6967279 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 fd816ec8e..391138ecc 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 3d3d380e8..989459270 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 0fc2d2641..b278c0b55 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 ecba56fa9..39d0efc62 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 5918b2da6..1e4f7ab73 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 54be93439..171aa9ec2 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 5b3f7a892..cb91a9ea1 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 0360699b5..8f90a6d7d 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 1894c0ef3..84c31366f 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 fe3299b46..2e4aac93a 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 328eb37b1..9cba35ad1 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 e6e4b97bb..f0da9f429 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 ee686541f..72709a0fe 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 04f9ad821..eabc54c36 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 ca2e54fe4..07a652fd7 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 670d9ad44..9de99a68b 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 599d525b0..c45cff4f3 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 02e32c54b..66dfd9818 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 9df42e002..af7b94bdf 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 c5bcf5942..f904065f0 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 358c8d3c1..608a6342e 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 6bbc8eb16..7dfbe93cf 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 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..8af5868fd8578ee6af2a6e381d7b902c398dd1cc 100644 GIT binary patch literal 16388 zcmeHO%WfQ15WQn3j`7$Yzw=Ot3=3G zOw zp^al4+o6XMj={AWp5l%@%M^ZsAm1sb5Ee0Z#GV0pc*)koPdm7B1WmhAjeb&)s?2mG z($?Q<4b&Q_HBf8dP1FE$H$LMk*T*aM_gVwB2HtoLFg`5f;CVeeU&OP74t!(z%)hgQ zmJfbLSuEF&v_y=k@G-5zi4G8WA2E*~r|u@Y9gd zabCtR_%zn#RDY{AP;0=^z#6VCsr_mjd&(Ymyiag!WB19QJE1MR)-KM4U2Eg*!mhQc zxK4px#qVVlcFkt5yBNO4UYjF6!`}7;qjKNNeeNc9x<~j++h-ctD=^&{f@ve1g*BhA z@kfF0W|D7P^wP#YJnrmwe8!Pqhh}8wWDj4vnDa(@&Z9pU7;h(KEHhTjom~TCmFr2~ zBglAyuT7EGwXyUw{#5;pTSKx{NEkkbgqkxgVX=3U@$Uwsx7ZN>X~d=6y@-!_~hFPd>pKDXewzWQ}Nw>|5xO9RT$LD5pav z-dc@vY(q<5Z7c7YEsV8+5%&=@jxf60PZMv}<4%KtD~sJc#+75NTXyxl(b0=DqfZ*_ zR=DTSmDaDseUQK*9vWQBjK@gB4&bpbEn}8J&lz+VGwuS-J|?4;J7ap8;ka%%P9zr` z0=u}*Y7 zSv*0b#iYN8TjHMSzzj&+7t9U&?Py-H>asDZ1K z>&!4LBDb=JWIfqNb;JnH`jT}^tX5-;+b!-y808F83k<9thN?~(!3??yG-Jd*Vg+k# z#>(=T$aV&G3#c<7SXb&wgEb4E)w9-Sl&{K#$Y+Lg7SVkZD0V{f_YYV}FoU~RVKy}k zQlk&zDzXJSI`(}7#!7>1X2_@qW+f-5i4A{Hv9gRx`A#*;$=hOOjiPpT zq1Rl(fgZ;^V&*sohE(RHef4M)axXJJ>q$S`(vvji9bjVijMj_qfTVkB11nsyuO8w3 ze=x#8eX$}i^PzQFL9??^Szj5{G;|j2#5(o~yq7&KxBe6h>G>@?*bdgh4&-3BiM>ef zF3xdZths58 z>OB$Xt;fCwo>OSc^+Q&t=I%*I$@qN$pW~i1(KFzRsl+8Z23*&bxck0;FlrmzP0=P- zM(iUNcPM#gdLjJ-$K$vH4da~><5R5v*o%)@GFsn0A4vPp@5+D5(iS_bamSvNR@Sp- ze77X+DeM({J?!`OcWJCO@|;TlkMHz38j&@q$7o@Ur-(l6ZZH<*{Rua(%6il2`2w1I z5&AA;BW3b43;j{EEMP82K=O^ieBa6SH$MI499%tk=#$Li0-*wR%EYoS88FWs-;g| zxud*dA;A)BJNmxs5sVzBlW;4+!kSij+I>(vc~<1BNKhb$TTkT zh;@Lgajve>kCd0_`yH>M{>=Tem;8K2$x4R#kbX`7@)+P>hJhAff0))QGVpmb`wNT> z4i|mgaNVlp;=Yz+Y~za^8=n)tYdB{yBSt$$DOMo;ij^_mu>rl&K zFvl3|ayDlC{|V}c-5Y9zd55fhd35!d;Aj3Z{PW?4gsG#B;T1~hB7RC2dE=+kF{KSrRcG* z0mZ!NdXZ>I-#wn)&HYtECnlrMX7|T6!Mgh#HfME~u&k_1J>m_;5POaLnT1)koq z&D{Bl7gzI@_d?MJd_H=%=9|2fud=omkJY$bGjSF1sVTjdF(;uJ+S}-MIvcrIGdp%+ zHZ<6U49gu+-Z_nW$k&X!Vi;!)Pd*Fo9E&U1XF%kwV9*gEp$A&jFmUzIgQE=jijPlo z;}ck?zGH+kEb$Gx_yjrG_oWpT>J|3?$opfl7I@gi`P20O|HZrN@3jVM4ZM~bXk2;x z#p6eqS|c$%zH7gX<0~Be@V*QFfDe9_SMhO{SMhN+|16B(*R%UB;u%-)`Op6WAjal8 W|FdGP^M7D{r!r^=wt{%=^Z(z)c%2CV delta 126 zcmZo^U~DmvU|?WibSh0TWMB{gGC6=4L^G&PEEJolCpp=`hJ^>j2LVQ)+-5*aY zn07HPX6N7#WQNEHa06*qAXWv+e`lV|Z==WwRLzh8#0-;7c$9gVAZCNCW7r(eGlv-f DZrTwl diff --git a/index.html b/index.html index f65ccafdf..e876ece21 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), 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 c0a5b1c12..b0ca55a37 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 46a0368aa..224f40637 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 0c6a21eb3..1676547a8 100644 --- a/search.html +++ b/search.html @@ -15,8 +15,8 @@ - - + +