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/404.html b/404.html index 821333edb..758a6a309 100644 --- a/404.html +++ b/404.html @@ -2,7 +2,7 @@
- +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.
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.
["'])(?.*?)\1/,p=/\{(? [\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:""},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"vbnet":case"vba":case"visual-basic":return g(["vb","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 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 y={codeBlockContent:"codeBlockContent_biex",codeBlockTitle:"codeBlockTitle_Ktv7",codeBlock:"codeBlock_bY9V",codeBlockStandalone:"codeBlockStandalone_MEMb",codeBlockLines:"codeBlockLines_e6Vv",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_o6Pm",buttonGroup:"buttonGroup__atx"};function v(e){let{children:t,className:n}=e;return(0,B.jsx)(j,{as:"pre",tabIndex:0,className:(0,c.Z)(y.codeBlockStandalone,"thin-scrollbar",n),children:(0,B.jsx)("code",{className:y.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,key:t})},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:v}=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:y.codeBlockTitle,children:g}),(0,B.jsxs)("div",{className:y.codeBlockContent,children:[(0,B.jsx)(L.y$,{theme:f,code:v,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,y.codeBlock,"thin-scrollbar"),style:n,children:(0,B.jsx)("code",{className:(0,c.Z)(y.codeBlockLines,C&&y.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:y.buttonGroup,children:[(h.isEnabled||h.isCodeScrollable)&&(0,B.jsx)(Z,{className:y.codeButton,onClick:()=>h.toggle(),isEnabled:h.isEnabled}),(0,B.jsx)($,{className:y.codeButton,code:v})]})]})]})}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:v;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 {"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.5e0bc1d3.js b/assets/js/1772.5e0bc1d3.js deleted file mode 100644 index 424383eab..000000000 --- a/assets/js/1772.5e0bc1d3.js +++ /dev/null @@ -1 +0,0 @@ -"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(10833),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.4a2f1cff.js b/assets/js/17896441.4a2f1cff.js deleted file mode 100644 index 9f7b11593..000000000 --- a/assets/js/17896441.4a2f1cff.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7918],{78945:(e,t,a)=>{a.r(t),a.d(t,{default:()=>ce});var s=a(67294),n=a(10833),i=a(902),l=a(85893);const o=s.createContext(null);function r(e){let{children:t,content:a}=e;const n=function(e){return(0,s.useMemo)((()=>({metadata:e.metadata,frontMatter:e.frontMatter,assets:e.assets,contentTitle:e.contentTitle,toc:e.toc})),[e])}(a);return(0,l.jsx)(o.Provider,{value:n,children:t})}function d(){const e=(0,s.useContext)(o);if(null===e)throw new i.i6("DocProvider");return e}function c(){const{metadata:e,frontMatter:t,assets:a}=d();return(0,l.jsx)(n.d,{title:e.title,description:e.description,keywords:t.keywords,image:a.image??t.image})}var u=a(36905),h=a(87524),m=a(95999),p=a(32244);function b(e){const{previous:t,next:a}=e;return(0,l.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,m.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"}),children:[t&&(0,l.jsx)(p.Z,{...t,subLabel:(0,l.jsx)(m.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc",children:"Previous"})}),a&&(0,l.jsx)(p.Z,{...a,subLabel:(0,l.jsx)(m.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc",children:"Next"}),isNext:!0})]})}function x(){const{metadata:e}=d();return(0,l.jsx)(b,{previous:e.previous,next:e.next})}var v=a(52263),g=a(39960),j=a(80143),f=a(35281),_=a(60373),Z=a(74477);const N={unreleased:function(e){let{siteTitle:t,versionMetadata:a}=e;return(0,l.jsx)(m.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:a.label})},children:"This is unreleased documentation for {siteTitle} {versionLabel} version."})},unmaintained:function(e){let{siteTitle:t,versionMetadata:a}=e;return(0,l.jsx)(m.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:a.label})},children:"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained."})}};function L(e){const t=N[e.versionMetadata.banner];return(0,l.jsx)(t,{...e})}function k(e){let{versionLabel:t,to:a,onClick:s}=e;return(0,l.jsx)(m.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:a,onClick:s,children:(0,l.jsx)(m.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 C(e){let{className:t,versionMetadata:a}=e;const{siteConfig:{title:s}}=(0,v.Z)(),{pluginId:n}=(0,j.gA)({failfast:!0}),{savePreferredVersionName:i}=(0,_.J)(n),{latestDocSuggestion:o,latestVersionSuggestion:r}=(0,j.Jo)(n),d=o??(c=r).docs.find((e=>e.id===c.mainDocId));var c;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)(L,{siteTitle:s,versionMetadata:a})}),(0,l.jsx)("div",{className:"margin-top--md",children:(0,l.jsx)(k,{versionLabel:r.label,to:d.path,onClick:()=>i(r.name)})})]})}function T(e){let{className:t}=e;const a=(0,Z.E)();return a.banner?(0,l.jsx)(C,{className:t,versionMetadata:a}):null}function U(e){let{className:t}=e;const a=(0,Z.E)();return a.badge?(0,l.jsx)("span",{className:(0,u.Z)(t,f.k.docs.docVersionBadge,"badge badge--secondary"),children:(0,l.jsx)(m.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:a.label},children:"Version: {versionLabel}"})}):null}function w(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:a}=e;return(0,l.jsx)(m.Z,{id:"theme.lastUpdated.atDate",description:"The words used to describe on which date a page has been last updated",values:{date:(0,l.jsx)("b",{children:(0,l.jsx)("time",{dateTime:new Date(1e3*t).toISOString(),children:a})})},children:" on {date}"})}function y(e){let{lastUpdatedBy:t}=e;return(0,l.jsx)(m.Z,{id:"theme.lastUpdated.byUser",description:"The words used to describe by who the page has been last updated",values:{user:(0,l.jsx)("b",{children:t})},children:" by {user}"})}function A(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:a,lastUpdatedBy:s}=e;return(0,l.jsxs)("span",{className:f.k.common.lastUpdated,children:[(0,l.jsx)(m.Z,{id:"theme.lastUpdated.lastUpdatedAtBy",description:"The sentence used to display when a page has been last updated, and by who",values:{atDate:t&&a?(0,l.jsx)(w,{lastUpdatedAt:t,formattedLastUpdatedAt:a}):"",byUser:s?(0,l.jsx)(y,{lastUpdatedBy:s}):""},children:"Last updated{atDate}{byUser}"}),!1]})}var M=a(84881),B=a(71526);const I={lastUpdated:"lastUpdated_vwxv"};function E(e){return(0,l.jsx)("div",{className:(0,u.Z)(f.k.docs.docFooterTagsRow,"row margin-bottom--sm"),children:(0,l.jsx)("div",{className:"col",children:(0,l.jsx)(B.Z,{...e})})})}function V(e){let{editUrl:t,lastUpdatedAt:a,lastUpdatedBy:s,formattedLastUpdatedAt:n}=e;return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docFooterEditMetaRow,"row"),children:[(0,l.jsx)("div",{className:"col",children:t&&(0,l.jsx)(M.Z,{editUrl:t})}),(0,l.jsx)("div",{className:(0,u.Z)("col",I.lastUpdated),children:(a||s)&&(0,l.jsx)(A,{lastUpdatedAt:a,formattedLastUpdatedAt:n,lastUpdatedBy:s})})]})}function H(){const{metadata:e}=d(),{editUrl:t,lastUpdatedAt:a,formattedLastUpdatedAt:s,lastUpdatedBy:n,tags:i}=e,o=i.length>0,r=!!(t||a||n);return o||r?(0,l.jsxs)("footer",{className:(0,u.Z)(f.k.docs.docFooter,"docusaurus-mt-lg"),children:[o&&(0,l.jsx)(E,{tags:i}),r&&(0,l.jsx)(V,{editUrl:t,lastUpdatedAt:a,lastUpdatedBy:n,formattedLastUpdatedAt:s})]}):null}var P=a(86043),D=a(93743);const S={tocCollapsibleButton:"tocCollapsibleButton_TO0P",tocCollapsibleButtonExpanded:"tocCollapsibleButtonExpanded_MG3E"};function F(e){let{collapsed:t,...a}=e;return(0,l.jsx)("button",{type:"button",...a,className:(0,u.Z)("clean-btn",S.tocCollapsibleButton,!t&&S.tocCollapsibleButtonExpanded,a.className),children:(0,l.jsx)(m.Z,{id:"theme.TOCCollapsible.toggleButtonLabel",description:"The label used by the button on the collapsible TOC component",children:"On this page"})})}const R={tocCollapsible:"tocCollapsible_ETCw",tocCollapsibleContent:"tocCollapsibleContent_vkbj",tocCollapsibleExpanded:"tocCollapsibleExpanded_sAul"};function z(e){let{toc:t,className:a,minHeadingLevel:s,maxHeadingLevel:n}=e;const{collapsed:i,toggleCollapsed:o}=(0,P.u)({initialState:!0});return(0,l.jsxs)("div",{className:(0,u.Z)(R.tocCollapsible,!i&&R.tocCollapsibleExpanded,a),children:[(0,l.jsx)(F,{collapsed:i,onClick:o}),(0,l.jsx)(P.z,{lazy:!0,className:R.tocCollapsibleContent,collapsed:i,children:(0,l.jsx)(D.Z,{toc:t,minHeadingLevel:s,maxHeadingLevel:n})})]})}const O={tocMobile:"tocMobile_ITEo"};function G(){const{toc:e,frontMatter:t}=d();return(0,l.jsx)(z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:(0,u.Z)(f.k.docs.docTocMobile,O.tocMobile)})}var W=a(39407);function q(){const{toc:e,frontMatter:t}=d();return(0,l.jsx)(W.Z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:f.k.docs.docTocDesktop})}var J=a(92503),Q=a(40591);function X(e){let{children:t}=e;const a=function(){const{metadata:e,frontMatter:t,contentTitle:a}=d();return t.hide_title||void 0!==a?null:e.title}();return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docMarkdown,"markdown"),children:[a&&(0,l.jsx)("header",{children:(0,l.jsx)(J.Z,{as:"h1",children:a})}),(0,l.jsx)(Q.Z,{children:t})]})}var Y=a(53438),$=a(48596),K=a(44996);function ee(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 te={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function ae(){const e=(0,K.Z)("/");return(0,l.jsx)("li",{className:"breadcrumbs__item",children:(0,l.jsx)(g.Z,{"aria-label":(0,m.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)(ee,{className:te.breadcrumbHomeIcon})})})}const se={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function ne(e){let{children:t,href:a,isLast:s}=e;const n="breadcrumbs__link";return s?(0,l.jsx)("span",{className:n,itemProp:"name",children:t}):a?(0,l.jsx)(g.Z,{className:n,href:a,itemProp:"item",children:(0,l.jsx)("span",{itemProp:"name",children:t})}):(0,l.jsx)("span",{className:n,children:t})}function ie(e){let{children:t,active:a,index:s,addMicrodata:n}=e;return(0,l.jsxs)("li",{...n&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},className:(0,u.Z)("breadcrumbs__item",{"breadcrumbs__item--active":a}),children:[t,(0,l.jsx)("meta",{itemProp:"position",content:String(s+1)})]})}function le(){const e=(0,Y.s1)(),t=(0,$.Ns)();return e?(0,l.jsx)("nav",{className:(0,u.Z)(f.k.docs.docBreadcrumbs,se.breadcrumbsContainer),"aria-label":(0,m.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)(ae,{}),e.map(((t,a)=>{const s=a===e.length-1,n="category"===t.type&&t.linkUnlisted?void 0:t.href;return(0,l.jsx)(ie,{active:s,index:a,addMicrodata:!!n,children:(0,l.jsx)(ne,{href:n,isLast:s,children:t.label})},a)}))]})}):null}var oe=a(22212);const re={docItemContainer:"docItemContainer_Djhp",docItemCol:"docItemCol_VOVn"};function de(e){let{children:t}=e;const a=function(){const{frontMatter:e,toc:t}=d(),a=(0,h.i)(),s=e.hide_table_of_contents,n=!s&&t.length>0;return{hidden:s,mobile:n?(0,l.jsx)(G,{}):void 0,desktop:!n||"desktop"!==a&&"ssr"!==a?void 0:(0,l.jsx)(q,{})}}(),{metadata:{unlisted:s}}=d();return(0,l.jsxs)("div",{className:"row",children:[(0,l.jsxs)("div",{className:(0,u.Z)("col",!a.hidden&&re.docItemCol),children:[s&&(0,l.jsx)(oe.Z,{}),(0,l.jsx)(T,{}),(0,l.jsxs)("div",{className:re.docItemContainer,children:[(0,l.jsxs)("article",{children:[(0,l.jsx)(le,{}),(0,l.jsx)(U,{}),a.mobile,(0,l.jsx)(X,{children:t}),(0,l.jsx)(H,{})]}),(0,l.jsx)(x,{})]})]}),a.desktop&&(0,l.jsx)("div",{className:"col col--3",children:a.desktop})]})}function ce(e){const t=`docs-doc-id-${e.content.metadata.id}`,a=e.content;return(0,l.jsx)(r,{content:e.content,children:(0,l.jsxs)(n.FG,{className:t,children:[(0,l.jsx)(c,{}),(0,l.jsx)(de,{children:(0,l.jsx)(a,{})})]})})}},84881:(e,t,a)=>{a.d(t,{Z:()=>c});a(67294);var s=a(95999),n=a(35281),i=a(39960),l=a(36905);const o={iconEdit:"iconEdit_Z9Sw"};var r=a(85893);function d(e){let{className:t,...a}=e;return(0,r.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,l.Z)(o.iconEdit,t),"aria-hidden":"true",...a,children:(0,r.jsx)("g",{children:(0,r.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 c(e){let{editUrl:t}=e;return(0,r.jsxs)(i.Z,{to:t,className:n.k.common.editThisPage,children:[(0,r.jsx)(d,{}),(0,r.jsx)(s.Z,{id:"theme.common.editThisPage",description:"The link label to edit the current page",children:"Edit this page"})]})}},32244:(e,t,a)=>{a.d(t,{Z:()=>l});a(67294);var s=a(36905),n=a(39960),i=a(85893);function l(e){const{permalink:t,title:a,subLabel:l,isNext:o}=e;return(0,i.jsxs)(n.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:a})]})}},13008:(e,t,a)=>{a.d(t,{Z:()=>o});a(67294);var s=a(36905),n=a(39960);const i={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=a(85893);function o(e){let{permalink:t,label:a,count:o}=e;return(0,l.jsxs)(n.Z,{href:t,className:(0,s.Z)(i.tag,o?i.tagWithCount:i.tagRegular),children:[a,o&&(0,l.jsx)("span",{children:o})]})}},71526:(e,t,a)=>{a.d(t,{Z:()=>r});a(67294);var s=a(36905),n=a(95999),i=a(13008);const l={tags:"tags_jXut",tag:"tag_QGVx"};var o=a(85893);function r(e){let{tags:t}=e;return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("b",{children:(0,o.jsx)(n.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:a}=e;return(0,o.jsx)("li",{className:l.tag,children:(0,o.jsx)(i.Z,{label:t,permalink:a})},a)}))})]})}}}]); \ No newline at end of file diff --git a/assets/js/17896441.b4b61076.js b/assets/js/17896441.b4b61076.js new file mode 100644 index 000000000..5fed32b09 --- /dev/null +++ b/assets/js/17896441.b4b61076.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7918],{47488:(e,t,a)=>{a.r(t),a.d(t,{default:()=>ce});var s=a(67294),n=a(44873),i=a(93478),l=a(85893);const o=s.createContext(null);function r(e){let{children:t,content:a}=e;const n=function(e){return(0,s.useMemo)((()=>({metadata:e.metadata,frontMatter:e.frontMatter,assets:e.assets,contentTitle:e.contentTitle,toc:e.toc})),[e])}(a);return(0,l.jsx)(o.Provider,{value:n,children:t})}function d(){const e=(0,s.useContext)(o);if(null===e)throw new i.i6("DocProvider");return e}function c(){const{metadata:e,frontMatter:t,assets:a}=d();return(0,l.jsx)(n.d,{title:e.title,description:e.description,keywords:t.keywords,image:a.image??t.image})}var u=a(36905),h=a(94980),m=a(11614),p=a(16948);function b(e){const{previous:t,next:a}=e;return(0,l.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,m.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"}),children:[t&&(0,l.jsx)(p.Z,{...t,subLabel:(0,l.jsx)(m.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc",children:"Previous"})}),a&&(0,l.jsx)(p.Z,{...a,subLabel:(0,l.jsx)(m.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc",children:"Next"}),isNext:!0})]})}function x(){const{metadata:e}=d();return(0,l.jsx)(b,{previous:e.previous,next:e.next})}var v=a(6832),g=a(75013),j=a(4452),f=a(18015),_=a(4049),Z=a(6141);const N={unreleased:function(e){let{siteTitle:t,versionMetadata:a}=e;return(0,l.jsx)(m.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:a.label})},children:"This is unreleased documentation for {siteTitle} {versionLabel} version."})},unmaintained:function(e){let{siteTitle:t,versionMetadata:a}=e;return(0,l.jsx)(m.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:a.label})},children:"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained."})}};function L(e){const t=N[e.versionMetadata.banner];return(0,l.jsx)(t,{...e})}function k(e){let{versionLabel:t,to:a,onClick:s}=e;return(0,l.jsx)(m.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:a,onClick:s,children:(0,l.jsx)(m.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 C(e){let{className:t,versionMetadata:a}=e;const{siteConfig:{title:s}}=(0,v.Z)(),{pluginId:n}=(0,j.gA)({failfast:!0}),{savePreferredVersionName:i}=(0,_.J)(n),{latestDocSuggestion:o,latestVersionSuggestion:r}=(0,j.Jo)(n),d=o??(c=r).docs.find((e=>e.id===c.mainDocId));var c;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)(L,{siteTitle:s,versionMetadata:a})}),(0,l.jsx)("div",{className:"margin-top--md",children:(0,l.jsx)(k,{versionLabel:r.label,to:d.path,onClick:()=>i(r.name)})})]})}function T(e){let{className:t}=e;const a=(0,Z.E)();return a.banner?(0,l.jsx)(C,{className:t,versionMetadata:a}):null}function U(e){let{className:t}=e;const a=(0,Z.E)();return a.badge?(0,l.jsx)("span",{className:(0,u.Z)(t,f.k.docs.docVersionBadge,"badge badge--secondary"),children:(0,l.jsx)(m.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:a.label},children:"Version: {versionLabel}"})}):null}function w(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:a}=e;return(0,l.jsx)(m.Z,{id:"theme.lastUpdated.atDate",description:"The words used to describe on which date a page has been last updated",values:{date:(0,l.jsx)("b",{children:(0,l.jsx)("time",{dateTime:new Date(1e3*t).toISOString(),children:a})})},children:" on {date}"})}function y(e){let{lastUpdatedBy:t}=e;return(0,l.jsx)(m.Z,{id:"theme.lastUpdated.byUser",description:"The words used to describe by who the page has been last updated",values:{user:(0,l.jsx)("b",{children:t})},children:" by {user}"})}function A(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:a,lastUpdatedBy:s}=e;return(0,l.jsxs)("span",{className:f.k.common.lastUpdated,children:[(0,l.jsx)(m.Z,{id:"theme.lastUpdated.lastUpdatedAtBy",description:"The sentence used to display when a page has been last updated, and by who",values:{atDate:t&&a?(0,l.jsx)(w,{lastUpdatedAt:t,formattedLastUpdatedAt:a}):"",byUser:s?(0,l.jsx)(y,{lastUpdatedBy:s}):""},children:"Last updated{atDate}{byUser}"}),!1]})}var M=a(77612),B=a(58045);const I={lastUpdated:"lastUpdated_vwxv"};function E(e){return(0,l.jsx)("div",{className:(0,u.Z)(f.k.docs.docFooterTagsRow,"row margin-bottom--sm"),children:(0,l.jsx)("div",{className:"col",children:(0,l.jsx)(B.Z,{...e})})})}function V(e){let{editUrl:t,lastUpdatedAt:a,lastUpdatedBy:s,formattedLastUpdatedAt:n}=e;return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docFooterEditMetaRow,"row"),children:[(0,l.jsx)("div",{className:"col",children:t&&(0,l.jsx)(M.Z,{editUrl:t})}),(0,l.jsx)("div",{className:(0,u.Z)("col",I.lastUpdated),children:(a||s)&&(0,l.jsx)(A,{lastUpdatedAt:a,formattedLastUpdatedAt:n,lastUpdatedBy:s})})]})}function H(){const{metadata:e}=d(),{editUrl:t,lastUpdatedAt:a,formattedLastUpdatedAt:s,lastUpdatedBy:n,tags:i}=e,o=i.length>0,r=!!(t||a||n);return o||r?(0,l.jsxs)("footer",{className:(0,u.Z)(f.k.docs.docFooter,"docusaurus-mt-lg"),children:[o&&(0,l.jsx)(E,{tags:i}),r&&(0,l.jsx)(V,{editUrl:t,lastUpdatedAt:a,lastUpdatedBy:n,formattedLastUpdatedAt:s})]}):null}var P=a(17940),D=a(21351);const S={tocCollapsibleButton:"tocCollapsibleButton_TO0P",tocCollapsibleButtonExpanded:"tocCollapsibleButtonExpanded_MG3E"};function F(e){let{collapsed:t,...a}=e;return(0,l.jsx)("button",{type:"button",...a,className:(0,u.Z)("clean-btn",S.tocCollapsibleButton,!t&&S.tocCollapsibleButtonExpanded,a.className),children:(0,l.jsx)(m.Z,{id:"theme.TOCCollapsible.toggleButtonLabel",description:"The label used by the button on the collapsible TOC component",children:"On this page"})})}const R={tocCollapsible:"tocCollapsible_ETCw",tocCollapsibleContent:"tocCollapsibleContent_vkbj",tocCollapsibleExpanded:"tocCollapsibleExpanded_sAul"};function z(e){let{toc:t,className:a,minHeadingLevel:s,maxHeadingLevel:n}=e;const{collapsed:i,toggleCollapsed:o}=(0,P.u)({initialState:!0});return(0,l.jsxs)("div",{className:(0,u.Z)(R.tocCollapsible,!i&&R.tocCollapsibleExpanded,a),children:[(0,l.jsx)(F,{collapsed:i,onClick:o}),(0,l.jsx)(P.z,{lazy:!0,className:R.tocCollapsibleContent,collapsed:i,children:(0,l.jsx)(D.Z,{toc:t,minHeadingLevel:s,maxHeadingLevel:n})})]})}const O={tocMobile:"tocMobile_ITEo"};function G(){const{toc:e,frontMatter:t}=d();return(0,l.jsx)(z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:(0,u.Z)(f.k.docs.docTocMobile,O.tocMobile)})}var W=a(95967);function q(){const{toc:e,frontMatter:t}=d();return(0,l.jsx)(W.Z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:f.k.docs.docTocDesktop})}var J=a(34055),Q=a(48480);function X(e){let{children:t}=e;const a=function(){const{metadata:e,frontMatter:t,contentTitle:a}=d();return t.hide_title||void 0!==a?null:e.title}();return(0,l.jsxs)("div",{className:(0,u.Z)(f.k.docs.docMarkdown,"markdown"),children:[a&&(0,l.jsx)("header",{children:(0,l.jsx)(J.Z,{as:"h1",children:a})}),(0,l.jsx)(Q.Z,{children:t})]})}var Y=a(85919),$=a(18407),K=a(51402);function ee(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 te={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function ae(){const e=(0,K.Z)("/");return(0,l.jsx)("li",{className:"breadcrumbs__item",children:(0,l.jsx)(g.Z,{"aria-label":(0,m.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)(ee,{className:te.breadcrumbHomeIcon})})})}const se={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function ne(e){let{children:t,href:a,isLast:s}=e;const n="breadcrumbs__link";return s?(0,l.jsx)("span",{className:n,itemProp:"name",children:t}):a?(0,l.jsx)(g.Z,{className:n,href:a,itemProp:"item",children:(0,l.jsx)("span",{itemProp:"name",children:t})}):(0,l.jsx)("span",{className:n,children:t})}function ie(e){let{children:t,active:a,index:s,addMicrodata:n}=e;return(0,l.jsxs)("li",{...n&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},className:(0,u.Z)("breadcrumbs__item",{"breadcrumbs__item--active":a}),children:[t,(0,l.jsx)("meta",{itemProp:"position",content:String(s+1)})]})}function le(){const e=(0,Y.s1)(),t=(0,$.Ns)();return e?(0,l.jsx)("nav",{className:(0,u.Z)(f.k.docs.docBreadcrumbs,se.breadcrumbsContainer),"aria-label":(0,m.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)(ae,{}),e.map(((t,a)=>{const s=a===e.length-1,n="category"===t.type&&t.linkUnlisted?void 0:t.href;return(0,l.jsx)(ie,{active:s,index:a,addMicrodata:!!n,children:(0,l.jsx)(ne,{href:n,isLast:s,children:t.label})},a)}))]})}):null}var oe=a(94007);const re={docItemContainer:"docItemContainer_Djhp",docItemCol:"docItemCol_VOVn"};function de(e){let{children:t}=e;const a=function(){const{frontMatter:e,toc:t}=d(),a=(0,h.i)(),s=e.hide_table_of_contents,n=!s&&t.length>0;return{hidden:s,mobile:n?(0,l.jsx)(G,{}):void 0,desktop:!n||"desktop"!==a&&"ssr"!==a?void 0:(0,l.jsx)(q,{})}}(),{metadata:{unlisted:s}}=d();return(0,l.jsxs)("div",{className:"row",children:[(0,l.jsxs)("div",{className:(0,u.Z)("col",!a.hidden&&re.docItemCol),children:[s&&(0,l.jsx)(oe.Z,{}),(0,l.jsx)(T,{}),(0,l.jsxs)("div",{className:re.docItemContainer,children:[(0,l.jsxs)("article",{children:[(0,l.jsx)(le,{}),(0,l.jsx)(U,{}),a.mobile,(0,l.jsx)(X,{children:t}),(0,l.jsx)(H,{})]}),(0,l.jsx)(x,{})]})]}),a.desktop&&(0,l.jsx)("div",{className:"col col--3",children:a.desktop})]})}function ce(e){const t=`docs-doc-id-${e.content.metadata.id}`,a=e.content;return(0,l.jsx)(r,{content:e.content,children:(0,l.jsxs)(n.FG,{className:t,children:[(0,l.jsx)(c,{}),(0,l.jsx)(de,{children:(0,l.jsx)(a,{})})]})})}},77612:(e,t,a)=>{a.d(t,{Z:()=>c});a(67294);var s=a(11614),n=a(18015),i=a(75013),l=a(36905);const o={iconEdit:"iconEdit_Z9Sw"};var r=a(85893);function d(e){let{className:t,...a}=e;return(0,r.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,l.Z)(o.iconEdit,t),"aria-hidden":"true",...a,children:(0,r.jsx)("g",{children:(0,r.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 c(e){let{editUrl:t}=e;return(0,r.jsxs)(i.Z,{to:t,className:n.k.common.editThisPage,children:[(0,r.jsx)(d,{}),(0,r.jsx)(s.Z,{id:"theme.common.editThisPage",description:"The link label to edit the current page",children:"Edit this page"})]})}},16948:(e,t,a)=>{a.d(t,{Z:()=>l});a(67294);var s=a(36905),n=a(75013),i=a(85893);function l(e){const{permalink:t,title:a,subLabel:l,isNext:o}=e;return(0,i.jsxs)(n.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:a})]})}},24588:(e,t,a)=>{a.d(t,{Z:()=>o});a(67294);var s=a(36905),n=a(75013);const i={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=a(85893);function o(e){let{permalink:t,label:a,count:o}=e;return(0,l.jsxs)(n.Z,{href:t,className:(0,s.Z)(i.tag,o?i.tagWithCount:i.tagRegular),children:[a,o&&(0,l.jsx)("span",{children:o})]})}},58045:(e,t,a)=>{a.d(t,{Z:()=>r});a(67294);var s=a(36905),n=a(11614),i=a(24588);const l={tags:"tags_jXut",tag:"tag_QGVx"};var o=a(85893);function r(e){let{tags:t}=e;return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("b",{children:(0,o.jsx)(n.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:a}=e;return(0,o.jsx)("li",{className:l.tag,children:(0,o.jsx)(i.Z,{label:t,permalink:a})},a)}))})]})}}}]); \ No newline at end of file diff --git a/assets/js/18793598.7db5f06f.js b/assets/js/18793598.e88901d1.js similarity index 98% rename from assets/js/18793598.7db5f06f.js rename to assets/js/18793598.e88901d1.js index fa7dbf021..3aece3f55 100644 --- a/assets/js/18793598.7db5f06f.js +++ b/assets/js/18793598.e88901d1.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2278],{53225:(t,i,s)=>{s.r(i),s.d(i,{default:()=>d});var e=s(67294),h=s(85893);function n(t,i){return Math.floor(Math.random()*(i-t+1)+t)}function r(t,i,s,e){const h=e*Math.PI/180;return[t+s*Math.cos(h),i+s*Math.sin(h)]}function o(t,i,s,e){return 180*Math.atan2(e-i,s-t)/Math.PI}function c(t,i,s,e,h,n,r,o,c,a,u,d){this.ctx=t,this.init(i,s,e,h,n,r,o,c,a,u,d)}function a(t,i,s,e,h,n){this.ctx=t,this.init(i,s,e,h,n)}let u;if(c.prototype.init=function(t,i,s,e,h,n,r,o,c,a,u){this.X=t,this.Y=i,this.radius=h,this.x=s,this.y=e,this.r=n,this.w=r,this.c=u,this.rotate=o,this.speed=60*c,this.angleDiff=a,this.a=0},c.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=r(this.x,this.y,this.r,t),h=e[0],n=e[1],c=r(this.x,this.y,this.r,i),a=c[0],u=c[1],d=h-this.w,x=u-this.w,f=o(this.x,this.y,d,n),l=o(this.x,this.y,a,x),p=i*Math.PI/180,w=t*Math.PI/180,y=f*Math.PI/180,m=l*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()},c.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()},c.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},c.prototype.updateParams=function(t){this.a+=this.speed*t*this.radius/this.r},c.prototype.render=function(t){this.updateParams(t),this.draw()},a.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()}},a.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()},a.prototype.updatePosition=function(t){this.x+=this.v.x*t,this.y+=this.v.y*t},a.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)},a.prototype.render=function(t){this.updatePosition(t),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){u=new MutationObserver((function(t){t.forEach((function(t){"attributes"==t.type&&window.dispatchEvent(new Event("resized"))}))}));const t=document.querySelector("html");u.observe(t,{attributes:!0})}const d=t=>{const[i,r]=e.useState({x:1,y:1}),o=e.useRef(null),u=()=>{null!==o.current&&(o.current.width=o.current.clientWidth,o.current.height=o.current.clientHeight,r({x:o.current?o.current.clientWidth:0,y:o.current?o.current.clientHeight:0}))};return e.useEffect((()=>u()),[]),(s.g.window||process&&process.browser)&&(e.useEffect((()=>(window.addEventListener("resize",u),()=>window.removeEventListener("resize",u)))),e.useEffect((()=>(window.addEventListener("resized",u),()=>window.removeEventListener("resized",u))))),e.useEffect((()=>{!function(t,i,e,h){const r=t.getContext("2d"),o=i/2,u=e/2;let d,x;h?(d="#8d3838",x="#6e2b2b"):(d="#ffd4d4",x="#ffd4d4");const f=[],l=[],p=e/7,w=p/15,y=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 a(r,i,e,n(0,i),n(0,e),d);f.push(t)}l.push(new c(r,i,e,o,u,p,2.65*p,9*w,0,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,90,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,180,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,270,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,45,1.5,2,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,135,1.5,2,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,225,1.5,2,x));let m=0;y((function t(s){const h=(s-m)/1e3;r.clearRect(0,0,i,e);for(let i=0;i {s.r(i),s.d(i,{default:()=>d});var e=s(67294),h=s(85893);function n(t,i){return Math.floor(Math.random()*(i-t+1)+t)}function r(t,i,s,e){const h=e*Math.PI/180;return[t+s*Math.cos(h),i+s*Math.sin(h)]}function o(t,i,s,e){return 180*Math.atan2(e-i,s-t)/Math.PI}function c(t,i,s,e,h,n,r,o,c,a,u,d){this.ctx=t,this.init(i,s,e,h,n,r,o,c,a,u,d)}function a(t,i,s,e,h,n){this.ctx=t,this.init(i,s,e,h,n)}let u;if(c.prototype.init=function(t,i,s,e,h,n,r,o,c,a,u){this.X=t,this.Y=i,this.radius=h,this.x=s,this.y=e,this.r=n,this.w=r,this.c=u,this.rotate=o,this.speed=60*c,this.angleDiff=a,this.a=0},c.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=r(this.x,this.y,this.r,t),h=e[0],n=e[1],c=r(this.x,this.y,this.r,i),a=c[0],u=c[1],d=h-this.w,x=u-this.w,f=o(this.x,this.y,d,n),l=o(this.x,this.y,a,x),p=i*Math.PI/180,w=t*Math.PI/180,y=f*Math.PI/180,m=l*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()},c.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()},c.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},c.prototype.updateParams=function(t){this.a+=this.speed*t*this.radius/this.r},c.prototype.render=function(t){this.updateParams(t),this.draw()},a.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()}},a.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()},a.prototype.updatePosition=function(t){this.x+=this.v.x*t,this.y+=this.v.y*t},a.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)},a.prototype.render=function(t){this.updatePosition(t),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){u=new MutationObserver((function(t){t.forEach((function(t){"attributes"==t.type&&window.dispatchEvent(new Event("resized"))}))}));const t=document.querySelector("html");u.observe(t,{attributes:!0})}const d=t=>{const[i,r]=e.useState({x:1,y:1}),o=e.useRef(null),u=()=>{null!==o.current&&(o.current.width=o.current.clientWidth,o.current.height=o.current.clientHeight,r({x:o.current?o.current.clientWidth:0,y:o.current?o.current.clientHeight:0}))};return e.useEffect((()=>u()),[]),(s.g.window||process&&process.browser)&&(e.useEffect((()=>(window.addEventListener("resize",u),()=>window.removeEventListener("resize",u)))),e.useEffect((()=>(window.addEventListener("resized",u),()=>window.removeEventListener("resized",u))))),e.useEffect((()=>{!function(t,i,e,h){const r=t.getContext("2d"),o=i/2,u=e/2;let d,x;h?(d="#8d3838",x="#6e2b2b"):(d="#ffd4d4",x="#ffd4d4");const f=[],l=[],p=e/7,w=p/15,y=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 a(r,i,e,n(0,i),n(0,e),d);f.push(t)}l.push(new c(r,i,e,o,u,p,2.65*p,9*w,0,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,90,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,180,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,2.65*p,9*w,270,-1.5,0,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,45,1.5,2,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,135,1.5,2,x)),l.push(new c(r,i,e,o,u,p,1.45*p,8*w,225,1.5,2,x));let m=0;y((function t(s){const h=(s-m)/1e3;r.clearRect(0,0,i,e);for(let i=0;i {s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var i=s(85893),t=s(11151);const r={id:"engines",title:"Engines and scalability"},o=void 0,a={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"}},l={},d=[{value:"Memory engine",id:"memory-engine",level:2},{value:"Memory engine options",id:"memory-engine-options",level:3},{value:"history_meta_ttl",id:"history_meta_ttl",level:4},{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:"history_meta_ttl",id:"history_meta_ttl-1",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:"history_meta_ttl",id:"history_meta_ttl-2",level:4},{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, KeyDB, Tarantool engines available. And Nats broker which also supports at most once PUB/SUB."}),"\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.h3,{id:"memory-engine-options",children:"Memory engine options"}),"\n",(0,i.jsx)(n.h4,{id:"history_meta_ttl",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"})," (90 days)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," sets a time of history stream metadata expiration."]}),"\n",(0,i.jsx)(n.p,{children:"When using a history in a channel, Centrifugo keeps some metadata for it. 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 much bigger than maximum history TTL used in Centrifugo configuration."}),"\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:"1660",height:"987"})}),"\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.h4,{id:"history_meta_ttl-1",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"})," (90 days)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," sets a time of history stream metadata expiration."]}),"\n",(0,i.jsxs)(n.p,{children:["Similar to a Memory engine Redis engine also looks at ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," option. Meta key in Redis is a HASH that contains the current offset number in channel and the stream epoch value."]}),"\n",(0,i.jsx)(n.p,{children:"When using a history in a channel, Centrifugo saves metadata for it. 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. The rule of thumb here is to keep this value much bigger than maximum history TTL used in Centrifugo configuration."}),"\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 starting from DragonflyDB 1.3.0 and with ",(0,i.jsx)(n.code,{children:"redis_force_resp2"})," Centrifugo option on. 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"]}),"\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:"EXPERIMENTAL"})}),"\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:"caution",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"EXPERIMENTAL"})," status of Tarantool integration means that we are still going to improve it and there could be breaking changes as integration evolves."]})}),"\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.h4,{id:"history_meta_ttl-2",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Same option as for Memory engine and Redis engine also applies to Tarantool case."}),"\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-812f437e8d45aeb8ce3f5d9016db4569.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>a,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 a(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/192a8b1e.f596548c.js b/assets/js/192a8b1e.f596548c.js deleted file mode 100644 index 444db53ff..000000000 --- a/assets/js/192a8b1e.f596548c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5069],{47262:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var i=s(85893),t=s(11151);const r={id:"engines",title:"Engines and scalability"},o=void 0,a={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"}},l={},d=[{value:"Memory engine",id:"memory-engine",level:2},{value:"Memory engine options",id:"memory-engine-options",level:3},{value:"history_meta_ttl",id:"history_meta_ttl",level:4},{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:"history_meta_ttl",id:"history_meta_ttl-1",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:"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:"history_meta_ttl",id:"history_meta_ttl-2",level:4},{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, KeyDB, Tarantool engines available. And Nats broker which also supports at most once PUB/SUB."}),"\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.h3,{id:"memory-engine-options",children:"Memory engine options"}),"\n",(0,i.jsx)(n.h4,{id:"history_meta_ttl",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"})," (90 days)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," sets a time of history stream metadata expiration."]}),"\n",(0,i.jsx)(n.p,{children:"When using a history in a channel, Centrifugo keeps some metadata for it. 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 much bigger than maximum history TTL used in Centrifugo configuration."}),"\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:"1660",height:"987"})}),"\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.h4,{id:"history_meta_ttl-1",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"})," (90 days)."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"history_meta_ttl"})," sets a time of history stream metadata expiration."]}),"\n",(0,i.jsxs)(n.p,{children:["Similar to a Memory engine Redis engine also looks at ",(0,i.jsx)(n.code,{children:"history_meta_ttl"})," option. Meta key in Redis is a HASH that contains the current offset number in channel and the stream epoch value."]}),"\n",(0,i.jsx)(n.p,{children:"When using a history in a channel, Centrifugo saves metadata for it. 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. The rule of thumb here is to keep this value much bigger than maximum history TTL used in Centrifugo configuration."}),"\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.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 starting from DragonflyDB 1.3.0 and with ",(0,i.jsx)(n.code,{children:"redis_force_resp2"})," Centrifugo option on. 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"]}),"\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:"EXPERIMENTAL"})}),"\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:"caution",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"EXPERIMENTAL"})," status of Tarantool integration means that we are still going to improve it and there could be breaking changes as integration evolves."]})}),"\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.h4,{id:"history_meta_ttl-2",children:"history_meta_ttl"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"/docs/server/configuration#setting-time-duration-options",children:"Duration"}),", default ",(0,i.jsx)(n.code,{children:"2160h"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Same option as for Memory engine and Redis engine also applies to Tarantool case."}),"\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-812f437e8d45aeb8ce3f5d9016db4569.png"},11151:(e,n,s)=>{s.d(n,{Z:()=>a,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 a(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.d7d9efcb.js b/assets/js/19e7756f.d7d9efcb.js deleted file mode 100644 index 7622c2f66..000000000 --- a/assets/js/19e7756f.d7d9efcb.js +++ /dev/null @@ -1 +0,0 @@ -"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","\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.da453788.js b/assets/js/19e7756f.da453788.js new file mode 100644 index 000000000..4d9b3e261 --- /dev/null +++ b/assets/js/19e7756f.da453788.js @@ -0,0 +1 @@ +"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","\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.322c3e6b.js b/assets/js/1a4e3797.322c3e6b.js new file mode 100644 index 000000000..ffa78c8f1 --- /dev/null +++ b/assets/js/1a4e3797.322c3e6b.js @@ -0,0 +1,2 @@ +/*! For license information please see 1a4e3797.322c3e6b.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;u0&&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 {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{"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 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 {"use strict";e.exports=function(e,t){if(!Array.isArray(e))return-1;for(var r=0;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{"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 =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 =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 =0||(i[r]=e[r]);return i}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n =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 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 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.8530ad97.js.LICENSE.txt b/assets/js/1a4e3797.322c3e6b.js.LICENSE.txt similarity index 100% rename from assets/js/1a4e3797.8530ad97.js.LICENSE.txt rename to assets/js/1a4e3797.322c3e6b.js.LICENSE.txt diff --git a/assets/js/1a4e3797.8530ad97.js b/assets/js/1a4e3797.8530ad97.js deleted file mode 100644 index 1f0c0cbfc..000000000 --- a/assets/js/1a4e3797.8530ad97.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see 1a4e3797.8530ad97.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;u0&&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 {"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 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 {"use strict";e.exports=function(e,t){if(!Array.isArray(e))return-1;for(var r=0;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{"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 =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 =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 =0||(i[r]=e[r]);return i}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(n=0;n =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 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 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 {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/1f391b9e.0ccae32d.js b/assets/js/1f391b9e.0ccae32d.js deleted file mode 100644 index 00c44b91e..000000000 --- a/assets/js/1f391b9e.0ccae32d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3085],{14247:(e,a,s)=>{s.r(a),s.d(a,{default:()=>x});s(67294);var i=s(36905),n=s(10833),r=s(35281),c=s(7372),l=s(40591),t=s(39407),d=s(22212);const o={mdxPageWrapper:"mdxPageWrapper_j9I6"};var m=s(85893);function x(e){const{content:a}=e,{metadata:{title:s,description:x,frontMatter:g,unlisted:p},assets:h}=a,{keywords:j,wrapperClassName:v,hide_table_of_contents:_}=g,u=h.image??g.image;return(0,m.jsx)(n.FG,{className:(0,i.Z)(v??r.k.wrapper.mdxPages,r.k.page.mdxPage),children:(0,m.jsxs)(c.Z,{children:[(0,m.jsx)(n.d,{title:s,description:x,keywords:j,image:u}),(0,m.jsx)("main",{className:"container container--fluid margin-vert--lg",children:(0,m.jsxs)("div",{className:(0,i.Z)("row",o.mdxPageWrapper),children:[(0,m.jsxs)("div",{className:(0,i.Z)("col",!_&&"col--8"),children:[p&&(0,m.jsx)(d.Z,{}),(0,m.jsx)("article",{children:(0,m.jsx)(l.Z,{children:(0,m.jsx)(a,{})})})]}),!_&&a.toc.length>0&&(0,m.jsx)("div",{className:"col col--2",children:(0,m.jsx)(t.Z,{toc:a.toc,minHeadingLevel:g.toc_min_heading_level,maxHeadingLevel:g.toc_max_heading_level})})]})})]})})}}}]); \ No newline at end of file diff --git a/assets/js/1f391b9e.3ec4ad23.js b/assets/js/1f391b9e.3ec4ad23.js new file mode 100644 index 000000000..22d8c043a --- /dev/null +++ b/assets/js/1f391b9e.3ec4ad23.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3085],{29688:(e,a,s)=>{s.r(a),s.d(a,{default:()=>x});s(67294);var i=s(36905),n=s(44873),r=s(18015),c=s(78299),l=s(48480),t=s(95967),d=s(94007);const o={mdxPageWrapper:"mdxPageWrapper_j9I6"};var m=s(85893);function x(e){const{content:a}=e,{metadata:{title:s,description:x,frontMatter:g,unlisted:p},assets:h}=a,{keywords:j,wrapperClassName:v,hide_table_of_contents:_}=g,u=h.image??g.image;return(0,m.jsx)(n.FG,{className:(0,i.Z)(v??r.k.wrapper.mdxPages,r.k.page.mdxPage),children:(0,m.jsxs)(c.Z,{children:[(0,m.jsx)(n.d,{title:s,description:x,keywords:j,image:u}),(0,m.jsx)("main",{className:"container container--fluid margin-vert--lg",children:(0,m.jsxs)("div",{className:(0,i.Z)("row",o.mdxPageWrapper),children:[(0,m.jsxs)("div",{className:(0,i.Z)("col",!_&&"col--8"),children:[p&&(0,m.jsx)(d.Z,{}),(0,m.jsx)("article",{children:(0,m.jsx)(l.Z,{children:(0,m.jsx)(a,{})})})]}),!_&&a.toc.length>0&&(0,m.jsx)("div",{className:"col col--2",children:(0,m.jsx)(t.Z,{toc:a.toc,minHeadingLevel:g.toc_min_heading_level,maxHeadingLevel:g.toc_max_heading_level})})]})})]})})}}}]); \ No newline at end of file diff --git a/assets/js/211f1d7a.ee74edb8.js b/assets/js/211f1d7a.ee74edb8.js new file mode 100644 index 000000000..340dc8c12 --- /dev/null +++ b/assets/js/211f1d7a.ee74edb8.js @@ -0,0 +1 @@ +"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: Badchild <${"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 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 . 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 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 component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The 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,{...e,...n}),(0,b.jsx)(v,{...e,...n})]})}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": " "\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","\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.f10fc665.js b/assets/js/211f1d7a.f10fc665.js deleted file mode 100644 index 2f09c9a9b..000000000 --- a/assets/js/211f1d7a.f10fc665.js +++ /dev/null @@ -1 +0,0 @@ -"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(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": " "\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","\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 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 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 . 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 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 component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The 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,{...e,...n}),(0,b.jsx)(v,{...e,...n})]})}function w(e){const n=(0,f.Z)();return(0,b.jsx)(y,{...e,children:h(e.children)},String(n))}},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/230ca58d.5ccc6e1e.js b/assets/js/230ca58d.5ccc6e1e.js deleted file mode 100644 index f322f3573..000000000 --- a/assets/js/230ca58d.5ccc6e1e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4085],{81657:(e,r,i)=>{i.r(r),i.d(r,{assets:()=>c,contentTitle:()=>s,default:()=>d,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var n=i(85893),t=i(11151);const o={id:"performance",title:"Faster performance"},s=void 0,a={id:"pro/performance",title:"Faster performance",description:"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further.",source:"@site/docs/pro/performance.md",sourceDirName:"pro",slug:"/pro/performance",permalink:"/docs/pro/performance",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/performance.md",tags:[],version:"current",frontMatter:{id:"performance",title:"Faster performance"},sidebar:"Pro",previous:{title:"CEL expressions",permalink:"/docs/pro/cel_expressions"},next:{title:"Singleflight",permalink:"/docs/pro/singleflight"}},c={},l=[{value:"Faster HTTP API",id:"faster-http-api",level:2},{value:"Faster GRPC API",id:"faster-grpc-api",level:2},{value:"Faster HTTP proxy",id:"faster-http-proxy",level:2},{value:"Faster GRPC proxy",id:"faster-grpc-proxy",level:2},{value:"Faster JWT decoding",id:"faster-jwt-decoding",level:2},{value:"Faster GRPC unidirectional stream",id:"faster-grpc-unidirectional-stream",level:2},{value:"Examples",id:"examples",level:2},{value:"Publish HTTP API",id:"publish-http-api",level:3},{value:"History HTTP API",id:"history-http-api",level:3}];function p(e){const r={h2:"h2",h3:"h3",p:"p",...(0,t.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("img",{src:"/img/logo_animated_fast.svg",width:"100px",height:"100px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further."}),"\n",(0,n.jsx)(r.h2,{id:"faster-http-api",children:"Faster HTTP API"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized JSON serialization/deserialization for HTTP API."}),"\n",(0,n.jsx)(r.p,{children:"The effect can be noticeable under load. The exact numbers heavily depend on usage scenario. According to our benchmarks you can expect 10-15% more requests/sec for small message publications over HTTP API, and up to several times throughput boost when you are frequently get lots of messages from a history, see a couple of examples below."}),"\n",(0,n.jsx)(r.h2,{id:"faster-grpc-api",children:"Faster GRPC API"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf serialization/deserialization for GRPC API. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,n.jsx)(r.h2,{id:"faster-http-proxy",children:"Faster HTTP proxy"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized JSON serialization/deserialization for HTTP proxy. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,n.jsx)(r.h2,{id:"faster-grpc-proxy",children:"Faster GRPC proxy"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf serialization/deserialization for GRPC API. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,n.jsx)(r.h2,{id:"faster-jwt-decoding",children:"Faster JWT decoding"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized decoding of JWT claims."}),"\n",(0,n.jsx)(r.h2,{id:"faster-grpc-unidirectional-stream",children:"Faster GRPC unidirectional stream"}),"\n",(0,n.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf deserialization for GRPC unidirectional stream. This only affects deserialization of initial connect command."}),"\n",(0,n.jsx)(r.h2,{id:"examples",children:"Examples"}),"\n",(0,n.jsx)(r.p,{children:"Let's look at quick live comparisons of Centrifugo OSS and Centrifugo PRO regarding HTTP API performance."}),"\n",(0,n.jsx)(r.h3,{id:"publish-http-api",children:"Publish HTTP API"}),"\n",(0,n.jsxs)("video",{width:"100%",controls:!0,children:[(0,n.jsx)("source",{src:"/img/pro_api_publish_perf.mp4",type:"video/mp4"}),(0,n.jsx)(r.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,n.jsx)(r.p,{children:"In this video you can see a 13% speed up for publish operation. But for more complex API calls with larger payloads the difference can be much bigger. See next example that demonstrates this."}),"\n",(0,n.jsx)(r.h3,{id:"history-http-api",children:"History HTTP API"}),"\n",(0,n.jsxs)("video",{width:"100%",controls:!0,children:[(0,n.jsx)("source",{src:"/img/pro_api_history_perf.mp4",type:"video/mp4"}),(0,n.jsx)(r.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,n.jsx)(r.p,{children:"In this video you can see an almost 2x overall speed up while asking 100 messages from Centrifugo history API."})]})}function d(e={}){const{wrapper:r}={...(0,t.a)(),...e.components};return r?(0,n.jsx)(r,{...e,children:(0,n.jsx)(p,{...e})}):p(e)}},11151:(e,r,i)=>{i.d(r,{Z:()=>a,a:()=>s});var n=i(67294);const t={},o=n.createContext(t);function s(e){const r=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function a(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),n.createElement(o.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/230ca58d.85bd36e8.js b/assets/js/230ca58d.85bd36e8.js new file mode 100644 index 000000000..3498b029e --- /dev/null +++ b/assets/js/230ca58d.85bd36e8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4085],{81657:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>c,contentTitle:()=>s,default:()=>d,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var i=n(85893),t=n(11151);const o={id:"performance",title:"Faster performance"},s=void 0,a={id:"pro/performance",title:"Faster performance",description:"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further.",source:"@site/docs/pro/performance.md",sourceDirName:"pro",slug:"/pro/performance",permalink:"/docs/pro/performance",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/performance.md",tags:[],version:"current",frontMatter:{id:"performance",title:"Faster performance"},sidebar:"Pro",previous:{title:"Channel CEL expressions",permalink:"/docs/pro/cel_expressions"},next:{title:"Singleflight",permalink:"/docs/pro/singleflight"}},c={},l=[{value:"Faster HTTP API",id:"faster-http-api",level:2},{value:"Faster GRPC API",id:"faster-grpc-api",level:2},{value:"Faster HTTP proxy",id:"faster-http-proxy",level:2},{value:"Faster GRPC proxy",id:"faster-grpc-proxy",level:2},{value:"Faster JWT decoding",id:"faster-jwt-decoding",level:2},{value:"Faster GRPC unidirectional stream",id:"faster-grpc-unidirectional-stream",level:2},{value:"Examples",id:"examples",level:2},{value:"Publish HTTP API",id:"publish-http-api",level:3},{value:"History HTTP API",id:"history-http-api",level:3}];function p(e){const r={h2:"h2",h3:"h3",p:"p",...(0,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("img",{src:"/img/logo_animated_fast.svg",width:"100px",height:"100px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further."}),"\n",(0,i.jsx)(r.h2,{id:"faster-http-api",children:"Faster HTTP API"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized JSON serialization/deserialization for HTTP API."}),"\n",(0,i.jsx)(r.p,{children:"The effect can be noticeable under load. The exact numbers heavily depend on usage scenario. According to our benchmarks you can expect 10-15% more requests/sec for small message publications over HTTP API, and up to several times throughput boost when you are frequently get lots of messages from a history, see a couple of examples below."}),"\n",(0,i.jsx)(r.h2,{id:"faster-grpc-api",children:"Faster GRPC API"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf serialization/deserialization for GRPC API. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,i.jsx)(r.h2,{id:"faster-http-proxy",children:"Faster HTTP proxy"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized JSON serialization/deserialization for HTTP proxy. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,i.jsx)(r.h2,{id:"faster-grpc-proxy",children:"Faster GRPC proxy"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf serialization/deserialization for GRPC API. The effect can be noticeable under load. The exact numbers heavily depend on usage scenario."}),"\n",(0,i.jsx)(r.h2,{id:"faster-jwt-decoding",children:"Faster JWT decoding"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized decoding of JWT claims."}),"\n",(0,i.jsx)(r.h2,{id:"faster-grpc-unidirectional-stream",children:"Faster GRPC unidirectional stream"}),"\n",(0,i.jsx)(r.p,{children:"Centrifugo PRO has an optimized Protobuf deserialization for GRPC unidirectional stream. This only affects deserialization of initial connect command."}),"\n",(0,i.jsx)(r.h2,{id:"examples",children:"Examples"}),"\n",(0,i.jsx)(r.p,{children:"Let's look at quick live comparisons of Centrifugo OSS and Centrifugo PRO regarding HTTP API performance."}),"\n",(0,i.jsx)(r.h3,{id:"publish-http-api",children:"Publish HTTP API"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/pro_api_publish_perf.mp4",type:"video/mp4"}),(0,i.jsx)(r.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(r.p,{children:"In this video you can see a 13% speed up for publish operation. But for more complex API calls with larger payloads the difference can be much bigger. See next example that demonstrates this."}),"\n",(0,i.jsx)(r.h3,{id:"history-http-api",children:"History HTTP API"}),"\n",(0,i.jsxs)("video",{width:"100%",controls:!0,children:[(0,i.jsx)("source",{src:"/img/pro_api_history_perf.mp4",type:"video/mp4"}),(0,i.jsx)(r.p,{children:"Sorry, your browser doesn't support embedded video."})]}),"\n",(0,i.jsx)(r.p,{children:"In this video you can see an almost 2x overall speed up while asking 100 messages from Centrifugo history API."})]})}function d(e={}){const{wrapper:r}={...(0,t.a)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(p,{...e})}):p(e)}},11151:(e,r,n)=>{n.d(r,{Z:()=>a,a:()=>s});var i=n(67294);const t={},o=i.createContext(t);function s(e){const r=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function a(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),i.createElement(o.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2312.1c74a080.js b/assets/js/2312.1c74a080.js deleted file mode 100644 index 67bfaac56..000000000 --- a/assets/js/2312.1c74a080.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2312],{9286:(e,t,n)=>{"use strict";n.d(t,{Z:()=>H});var o=n(67294),s=n(72389),c=n(36905),a=n(92949),r=n(86668);function l(){const{prism:e}=(0,r.L)(),{colorMode:t}=(0,a.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=(? ["'])(?.*?)\1/,p=/\{(? [\d,-]+)\}/,f={js:{start:"\\/\\/",end:""},jsBlock:{start:"\\/\\*",end:"\\*\\/"},jsx:{start:"\\{\\s*\\/\\*",end:"\\*\\/\\s*\\}"},bash:{start:"#",end:""},html:{start:"\x3c!--",end:"--\x3e"},lua:{start:"--",end:""},wasm:{start:"\\;\\;",end:""},tex:{start:"%",end:""}};function b(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 h(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 a=function(e,t){switch(e){case"js":case"javascript":case"ts":case"typescript":return b(["js","jsBlock"],t);case"jsx":case"tsx":return b(["js","jsBlock","jsx"],t);case"html":return b(["js","jsBlock","html"],t);case"python":case"py":case"bash":return b(["bash"],t);case"markdown":case"md":return b(["html","jsx","bash"],t);case"tex":case"latex":case"matlab":return b(["tex"],t);case"lua":case"haskell":case"sql":return b(["lua"],t);case"wasm":return b(["wasm"],t);default:return b(Object.keys(f).filter((e=>!["lua","wasm","tex","latex","matlab"].includes(e))),t)}}(o,s),r=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 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},`),r.splice(d,1)}n=r.join("\n");const h={};return Object.entries(l).forEach((e=>{let[t,{range:n}]=e;d()(n).forEach((e=>{h[e]??=[],h[e].push(t)}))})),{lineClassNames:h,code:n}}const g={codeBlockContainer:"codeBlockContainer_Ckt0"};var k=n(85893);function x(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,k.jsx)(t,{...n,style:o,className:(0,c.Z)(n.className,g.codeBlockContainer,i.k.common.codeBlock)})}const B={codeBlockContent:"codeBlockContent_biex",codeBlockTitle:"codeBlockTitle_Ktv7",codeBlock:"codeBlock_bY9V",codeBlockStandalone:"codeBlockStandalone_MEMb",codeBlockLines:"codeBlockLines_e6Vv",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_o6Pm",buttonGroup:"buttonGroup__atx"};function j(e){let{children:t,className:n}=e;return(0,k.jsx)(x,{as:"pre",tabIndex:0,className:(0,c.Z)(B.codeBlockStandalone,"thin-scrollbar",n),children:(0,k.jsx)("code",{className:B.codeBlockLines,children:t})})}var y=n(902);const C={attributes:!0,characterData:!0,childList:!0,subtree:!0};function N(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=C);const s=(0,y.zX)(t),c=(0,y.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 v=n(14965);const w={codeLine:"codeLine_lJS_",codeLineNumber:"codeLineNumber_Tfdd",codeLineContent:"codeLineContent_feaV"};function L(e){let{line:t,classNames:n,showLineNumbers:o,getLineProps:s,getTokenProps:a}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const r=s({line:t,className:(0,c.Z)(n,o&&w.codeLine)}),l=t.map(((e,t)=>(0,k.jsx)("span",{...a({token:e,key:t})},t)));return(0,k.jsxs)("span",{...r,children:[o?(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)("span",{className:w.codeLineNumber}),(0,k.jsx)("span",{className:w.codeLineContent,children:l})]}):l,(0,k.jsx)("br",{})]})}var E=n(95999);function I(e){return(0,k.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,k.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 S(e){return(0,k.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,k.jsx)("path",{fill:"currentColor",d:"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"})})}const _={copyButtonCopied:"copyButtonCopied_obH4",copyButtonIcons:"copyButtonIcons_eSgA",copyButtonIcon:"copyButtonIcon_y97N",copyButtonSuccessIcon:"copyButtonSuccessIcon_LjdS"};function A(e){let{code:t,className:n}=e;const[s,a]=(0,o.useState)(!1),r=(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(),a=c.rangeCount>0&&c.getRangeAt(0);n.append(o),o.select(),o.selectionStart=0,o.selectionEnd=e.length;let r=!1;try{r=document.execCommand("copy")}catch{}o.remove(),a&&(c.removeAllRanges(),c.addRange(a)),s&&s.focus()}(t),a(!0),r.current=window.setTimeout((()=>{a(!1)}),1e3)}),[t]);return(0,o.useEffect)((()=>()=>window.clearTimeout(r.current)),[]),(0,k.jsx)("button",{type:"button","aria-label":s?(0,E.I)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,E.I)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,E.I)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,c.Z)("clean-btn",n,_.copyButton,s&&_.copyButtonCopied),onClick:l,children:(0,k.jsxs)("span",{className:_.copyButtonIcons,"aria-hidden":"true",children:[(0,k.jsx)(I,{className:_.copyButtonIcon}),(0,k.jsx)(S,{className:_.copyButtonSuccessIcon})]})})}function T(e){return(0,k.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,k.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 $={wordWrapButtonIcon:"wordWrapButtonIcon_Bwma",wordWrapButtonEnabled:"wordWrapButtonEnabled_EoeP"};function W(e){let{className:t,onClick:n,isEnabled:o}=e;const s=(0,E.I)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return(0,k.jsx)("button",{type:"button",onClick:n,className:(0,c.Z)("clean-btn",t,o&&$.wordWrapButtonEnabled),"aria-label":s,title:s,children:(0,k.jsx)(T,{className:$.wordWrapButtonIcon,"aria-hidden":"true"})})}function Z(e){let{children:t,className:n="",metastring:s,title:a,showLineNumbers:i,language:u}=e;const{prism:{defaultLanguage:d,magicComments:p}}=(0,r.L)(),f=function(e){return e?.toLowerCase()}(u??function(e){const t=e.split(" ").find((e=>e.startsWith("language-")));return t?.replace(/language-/,"")}(n)??d),b=l(),g=function(){const[e,t]=(0,o.useState)(!1),[n,s]=(0,o.useState)(!1),c=(0,o.useRef)(null),a=(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]),r=(0,o.useCallback)((()=>{const{scrollWidth:e,clientWidth:t}=c.current,n=e>t||c.current.querySelector("code").hasAttribute("style");s(n)}),[c]);return N(c,r),(0,o.useEffect)((()=>{r()}),[e,r]),(0,o.useEffect)((()=>(window.addEventListener("resize",r,{passive:!0}),()=>{window.removeEventListener("resize",r)})),[r]),{codeBlockRef:c,isEnabled:e,isCodeScrollable:n,toggle:a}}(),j=function(e){return e?.match(m)?.groups.title??""}(s)||a,{lineClassNames:y,code:C}=h(t,{metastring:s,language:f,magicComments:p}),w=i??function(e){return Boolean(e?.includes("showLineNumbers"))}(s);return(0,k.jsxs)(x,{as:"div",className:(0,c.Z)(n,f&&!n.includes(`language-${f}`)&&`language-${f}`),children:[j&&(0,k.jsx)("div",{className:B.codeBlockTitle,children:j}),(0,k.jsxs)("div",{className:B.codeBlockContent,children:[(0,k.jsx)(v.y$,{theme:b,code:C,language:f??"text",children:e=>{let{className:t,style:n,tokens:o,getLineProps:s,getTokenProps:a}=e;return(0,k.jsx)("pre",{tabIndex:0,ref:g.codeBlockRef,className:(0,c.Z)(t,B.codeBlock,"thin-scrollbar"),style:n,children:(0,k.jsx)("code",{className:(0,c.Z)(B.codeBlockLines,w&&B.codeBlockLinesWithNumbering),children:o.map(((e,t)=>(0,k.jsx)(L,{line:e,getLineProps:s,getTokenProps:a,classNames:y[t],showLineNumbers:w},t)))})})}}),(0,k.jsxs)("div",{className:B.buttonGroup,children:[(g.isEnabled||g.isCodeScrollable)&&(0,k.jsx)(W,{className:B.codeButton,onClick:()=>g.toggle(),isEnabled:g.isEnabled}),(0,k.jsx)(A,{className:B.codeButton,code:C})]})]})]})}function H(e){let{children:t,...n}=e;const c=(0,s.Z)(),a=function(e){return o.Children.toArray(e).some((e=>(0,o.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),r="string"==typeof a?Z:j;return(0,k.jsx)(r,{...n,children:a},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 {"use strict";n.d(t,{Z:()=>r,a:()=>a});var o=n(67294);const s={},c=o.createContext(s);function a(e){const t=o.useContext(c);return o.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(s):e.components||s:a(e.components),o.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2391cf3d.558435a6.js b/assets/js/2391cf3d.0da3dbbb.js similarity index 99% rename from assets/js/2391cf3d.558435a6.js rename to assets/js/2391cf3d.0da3dbbb.js index c922d8110..1fb27f6e0 100644 --- a/assets/js/2391cf3d.558435a6.js +++ b/assets/js/2391cf3d.0da3dbbb.js @@ -1 +1 @@ -"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 +"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 diff --git a/assets/js/2a42cb18.569b9e7e.js b/assets/js/2a42cb18.569b9e7e.js deleted file mode 100644 index 4c6146d3c..000000000 --- a/assets/js/2a42cb18.569b9e7e.js +++ /dev/null @@ -1 +0,0 @@ -"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","\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/2a42cb18.bf71becf.js b/assets/js/2a42cb18.bf71becf.js new file mode 100644 index 000000000..79579943f --- /dev/null +++ b/assets/js/2a42cb18.bf71becf.js @@ -0,0 +1 @@ +"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","\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/2dbf7ee0.ba4aa9c4.js b/assets/js/2dbf7ee0.2cb15b63.js similarity index 99% rename from assets/js/2dbf7ee0.ba4aa9c4.js rename to assets/js/2dbf7ee0.2cb15b63.js index 0a0998a7f..3bc30b6a1 100644 --- a/assets/js/2dbf7ee0.ba4aa9c4.js +++ b/assets/js/2dbf7ee0.2cb15b63.js @@ -1 +1 @@ -"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:()=>r,metadata:()=>o,toc:()=>l});var i=s(85893),t=s(11151);const r={id:"presence",title:"Online presence"},a=void 0,o={id:"server/presence",title:"Online presence",description:"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it.",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:"History and recovery",permalink:"/docs/server/history_and_recovery"},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,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it."}),"\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 "namespace": "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 "namespace": "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,t.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 t={},r=i.createContext(t);function a(e){const n=i.useContext(r);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(t):e.components||t: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([[3581],{86064:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>p,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var i=s(85893),t=s(11151);const r={id:"presence",title:"Online presence"},a=void 0,o={id:"server/presence",title:"Online presence",description:"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it.",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:"History and recovery",permalink:"/docs/server/history_and_recovery"},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,t.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it."}),"\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 "namespace": "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 "namespace": "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,t.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 t={},r=i.createContext(t);function a(e){const n=i.useContext(r);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(t):e.components||t:a(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2eb9c429.313fb402.js b/assets/js/2eb9c429.95a18056.js similarity index 98% rename from assets/js/2eb9c429.313fb402.js rename to assets/js/2eb9c429.95a18056.js index d339f812e..560055570 100644 --- a/assets/js/2eb9c429.313fb402.js +++ b/assets/js/2eb9c429.95a18056.js @@ -1 +1 @@ -"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 +"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 diff --git a/assets/js/2f70c421.34c9f5e8.js b/assets/js/2f70c421.34c9f5e8.js new file mode 100644 index 000000000..91ebd14c6 --- /dev/null +++ b/assets/js/2f70c421.34c9f5e8.js @@ -0,0 +1 @@ +"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 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 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 . 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 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 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: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,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}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","\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.db45968d.js b/assets/js/2f70c421.db45968d.js deleted file mode 100644 index 8ad3bb113..000000000 --- a/assets/js/2f70c421.db45968d.js +++ /dev/null @@ -1 +0,0 @@ -"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","\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)}},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 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 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 . 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 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 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: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,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function y(e){const n=(0,g.Z)();return(0,j.jsx)(w,{...e,children:u(e.children)},String(n))}},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/39d4d18a.9e507064.js b/assets/js/39d4d18a.61fa3e35.js similarity index 99% rename from assets/js/39d4d18a.9e507064.js rename to assets/js/39d4d18a.61fa3e35.js index f2ca3d464..8203bc509 100644 --- a/assets/js/39d4d18a.9e507064.js +++ b/assets/js/39d4d18a.61fa3e35.js @@ -1 +1 @@ -"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","\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],{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","\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/5072.311f97d1.js b/assets/js/5072.311f97d1.js new file mode 100644 index 000000000..d613f0280 --- /dev/null +++ b/assets/js/5072.311f97d1.js @@ -0,0 +1 @@ +"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(44873),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/58b29436.ba998a40.js b/assets/js/58b29436.ba998a40.js deleted file mode 100644 index 0222401d5..000000000 --- a/assets/js/58b29436.ba998a40.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9620],{74413:(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/docs/transports/client_api.md",sourceDirName:"transports",slug:"/transports/client_api",permalink:"/docs/transports/client_api",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/transports/client_api.md",tags:[],version:"current",frontMatter:{id:"client_api",title:"Client SDK API"},sidebar:"Transports",previous:{title:"Real-time transports",permalink:"/docs/transports/overview"},next:{title:"Client real-time SDKs",permalink:"/docs/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.jsxs)(n.p,{children:["This chapter describes the core concepts of client SDKs API. All our ",(0,s.jsx)(n.a,{href:"/docs/transports/client_sdk#list-of-client-sdks",children:"official real-time SDKs"})," follow this specification. This document describes behaviour visible to SDK user, if you want to find out low-level client protocol framing details \u2013 look at ",(0,s.jsx)(n.a,{href:"/docs/transports/client_protocol",children:"client protocol"})," document."]}),"\n",(0,s.jsxs)(n.p,{children:["Most examples here are written using our Javascript real-time SDK (",(0,s.jsx)(n.code,{children:"centrifuge-js"}),"), but all other Centrifugo connectors 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","\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. 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:"async function getToken() {\n if (!loggedIn) {\n return \"\"; // Empty token or pre-generated token for anonymous users.\n }\n // Fetch your application backend.\n const res = await fetch('http://localhost:8000/centrifugo/connection_token');\n if (!res.ok) {\n if (res.status === 403) {\n // Return special error to not proceed with token refreshes,\n // client will be disconnected.\n throw new Centrifuge.UnauthorizedError();\n }\n // Any other error thrown will result into token refresh re-attempts.\n throw new Error(`Unexpected status code ${res.status}`);\n }\n const data = await res.json();\n return data.token;\n}\n\nconst client = new Centrifuge(\n 'ws://localhost:8000/connection/websocket',\n {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE', // Optional, getToken is enough.\n getToken: getToken\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:"async function getToken(ctx) {\n // Fetch your application backend.\n const res = await fetch('http://localhost:8000/centrifugo/subscription_token', {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify({\n channel: ctx.channel\n })\n });\n if (!res.ok) {\n if (res.status === 403) {\n // Return special error to not proceed with token refreshes,\n // client will be disconnected.\n throw new Centrifuge.UnauthorizedError();\n }\n // Any other error thrown will result into token refresh re-attempts.\n throw new Error(`Unexpected status code ${res.status}`);\n }\n const data = await res.json();\n return data.token;\n}\n\nconst client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE', // Optional, getToken is enough.\n getToken: getToken\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/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/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 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 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 . 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 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 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: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,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function y(e){const n=(0,g.Z)();return(0,j.jsx)(w,{...e,children:u(e.children)},String(n))}},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/58b29436.d72498d7.js b/assets/js/58b29436.d72498d7.js new file mode 100644 index 000000000..e263688bf --- /dev/null +++ b/assets/js/58b29436.d72498d7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9620],{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 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 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 . 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 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 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: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,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function y(e){const n=(0,g.Z)();return(0,j.jsx)(w,{...e,children:u(e.children)},String(n))}},74413:(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/docs/transports/client_api.md",sourceDirName:"transports",slug:"/transports/client_api",permalink:"/docs/transports/client_api",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/transports/client_api.md",tags:[],version:"current",frontMatter:{id:"client_api",title:"Client SDK API"},sidebar:"Transports",previous:{title:"Real-time transports",permalink:"/docs/transports/overview"},next:{title:"Client real-time SDKs",permalink:"/docs/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.jsxs)(n.p,{children:["This chapter describes the core concepts of client SDKs API. All our ",(0,s.jsx)(n.a,{href:"/docs/transports/client_sdk#list-of-client-sdks",children:"official real-time SDKs"})," follow this specification. This document describes behaviour visible to SDK user, if you want to find out low-level client protocol framing details \u2013 look at ",(0,s.jsx)(n.a,{href:"/docs/transports/client_protocol",children:"client protocol"})," document."]}),"\n",(0,s.jsxs)(n.p,{children:["Most examples here are written using our Javascript real-time SDK (",(0,s.jsx)(n.code,{children:"centrifuge-js"}),"), but all other Centrifugo connectors 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","\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. 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:"async function getToken() {\n if (!loggedIn) {\n return \"\"; // Empty token or pre-generated token for anonymous users.\n }\n // Fetch your application backend.\n const res = await fetch('http://localhost:8000/centrifugo/connection_token');\n if (!res.ok) {\n if (res.status === 403) {\n // Return special error to not proceed with token refreshes,\n // client will be disconnected.\n throw new Centrifuge.UnauthorizedError();\n }\n // Any other error thrown will result into token refresh re-attempts.\n throw new Error(`Unexpected status code ${res.status}`);\n }\n const data = await res.json();\n return data.token;\n}\n\nconst client = new Centrifuge(\n 'ws://localhost:8000/connection/websocket',\n {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE', // Optional, getToken is enough.\n getToken: getToken\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:"async function getToken(ctx) {\n // Fetch your application backend.\n const res = await fetch('http://localhost:8000/centrifugo/subscription_token', {\n method: 'POST',\n headers: new Headers({ 'Content-Type': 'application/json' }),\n body: JSON.stringify({\n channel: ctx.channel\n })\n });\n if (!res.ok) {\n if (res.status === 403) {\n // Return special error to not proceed with token refreshes,\n // client will be disconnected.\n throw new Centrifuge.UnauthorizedError();\n }\n // Any other error thrown will result into token refresh re-attempts.\n throw new Error(`Unexpected status code ${res.status}`);\n }\n const data = await res.json();\n return data.token;\n}\n\nconst client = new Centrifuge('ws://localhost:8000/connection/websocket', {});\n\nconst sub = centrifuge.newSubscription(channel, {\n token: 'JWT-GENERATED-ON-BACKEND-SIDE', // Optional, getToken is enough.\n getToken: getToken\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/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/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/5de4a79c.4cea45af.js b/assets/js/5de4a79c.4cea45af.js new file mode 100644 index 000000000..1ee3a396b --- /dev/null +++ b/assets/js/5de4a79c.4cea45af.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8375],{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:()=>y});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 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 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 . 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 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 component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The 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 g=t(85893);function b(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,g.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,g.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,g.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function k(e){const n=j(e);return(0,g.jsxs)("div",{className:(0,s.Z)("tabs-container",m.tabList),children:[(0,g.jsx)(b,{...e,...n}),(0,g.jsx)(v,{...e,...n})]})}function y(e){const n=(0,f.Z)();return(0,g.jsx)(k,{...e,children:h(e.children)},String(n))}},62158:(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 JWT authentication"},c=void 0,l={id:"server/authentication",title:"Client JWT authentication",description:"To authenticate an incoming connection (client), Centrifugo can use a JSON Web Token (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism.",source:"@site/docs/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/authentication.md",tags:[],version:"current",frontMatter:{id:"authentication",title:"Client JWT authentication"},sidebar:"Guides",previous:{title:"Server API walkthrough",permalink:"/docs/server/server_api"},next:{title:"Channels and namespaces",permalink:"/docs/server/channels"}},d={},h=[{value:"Connection JWT Claims",id:"connection-jwt-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},{value:"Dynamic JWKs endpoint",id:"dynamic-jwks-endpoint",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 an incoming connection (client), Centrifugo can use a ",(0,i.jsx)(n.a,{href:"https://jwt.io/introduction",children:"JSON Web Token"})," (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["If you prefer not to use JWTs, consider the ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy feature"}),". It enables the proxying of connection requests from Centrifugo to your application's backend endpoint for authentication."]})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Using JWT for authentication can be beneficial in scenarios involving massive reconnects. As the authentication information is encoded in the token, this can significantly reduce the load on your application's session backend. For more details, refer to our ",(0,i.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"blog post"}),"."]})}),"\n",(0,i.jsx)(n.p,{children:"Upon connection, the client should supply a connection JWT containing several predefined credential claims. Below is a diagram illustrating this:"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{src:t(54740).Z+"",width:"2951",height:"1130"})}),"\n",(0,i.jsxs)(n.p,{children:["For more information about handling connection tokens on the client side, see the ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api#client-connection-token",children:"client SDK specification"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Currently, Centrifugo supports HMAC, RSA, and ECDSA JWT algorithms - specifically HS256, HS384, HS512, RSA256, RSA384, RSA512, EC256, EC384, and EC512."}),"\n",(0,i.jsxs)(n.p,{children:["Here, we will demonstrate example snippets using the Javascript Centrifugo client for the client-side and the ",(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 an HMAC secret key to Centrifugo, insert ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," into the 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": " "\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:"connection-jwt-claims",children:"Connection JWT Claims"}),"\n",(0,i.jsxs)(n.p,{children:["For connection JWT, Centrifugo uses some standard claims defined in ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"RFC 7519"}),", as well as custom Centrifugo-specific claims."]}),"\n",(0,i.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,i.jsxs)(n.p,{children:["This standard JWT claim must contain the ID of the current application user (",(0,i.jsx)(n.strong,{children:"as a string"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["If a user is not authenticated in the application but you wish to allow them to connect to Centrifugo, an empty string can be used as the user ID in the ",(0,i.jsx)(n.code,{children:"sub"})," claim. This facilitates anonymous access. In such cases, you might need to enable the corresponding channel namespace options that allow protocol features for anonymous users."]}),"\n",(0,i.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,i.jsx)(n.p,{children:"This claim specifies the UNIX timestamp (in seconds) when the token will expire. It is a standard JWT claim - all JWT libraries across different programming languages provide an API to set it."}),"\n",(0,i.jsxs)(n.p,{children:["If the ",(0,i.jsx)(n.code,{children:"exp"})," claim is not included, Centrifugo will not expire the connection. When included, a special algorithm will identify connections with an ",(0,i.jsx)(n.code,{children:"exp"})," in the past and initiate the connection refresh mechanism. The refresh mechanism allows a connection to be extended. If the refresh fails, Centrifugo will eventually close the client connection, which will not be accepted again until new valid and current credentials are provided in the connection token."]}),"\n",(0,i.jsx)(n.p,{children:"The connection expiration mechanism can be utilized in scenarios where you do not want users to remain subscribed to channels after being banned or deactivated in the application. It also serves to protect users from token leakage by setting a reasonably short expiration time."}),"\n",(0,i.jsxs)(n.p,{children:["Choose the ",(0,i.jsx)(n.code,{children:"exp"})," value judiciously; too short a value can lead to frequent application hits with refresh requests, whereas too long a value can result in delayed user connection deactivation. It's a matter of balance."]}),"\n",(0,i.jsxs)(n.p,{children:["Further details on connection expiration can be found ",(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 represents the UNIX time when the token was issued (in seconds). Refer to the ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6",children:"definition in RFC 7519"}),". This claim is optional but can be advantageous in conjunction with ",(0,i.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO's token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,i.jsxs)(n.p,{children:["This is a unique identifier for the token. Refer to the ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7",children:"definition in RFC 7519"}),". This claim is optional but can be beneficial in conjunction with ",(0,i.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO's token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"aud",children:"aud"}),"\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/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})}),"\n",(0,i.jsx)(n.h3,{id:"iss",children:"iss"}),"\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/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})}),"\n",(0,i.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,i.jsx)(n.p,{children:"This optional claim provides additional information about the client's connection for Centrifugo. This information will be included in presence data, join/leave events, and client-side channel publications."}),"\n",(0,i.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,i.jsxs)(n.p,{children:["For those utilizing the binary Protobuf protocol and requiring the ",(0,i.jsx)(n.code,{children:"info"})," to be custom bytes, this field should be used."]}),"\n",(0,i.jsxs)(n.p,{children:["It contains a ",(0,i.jsx)(n.code,{children:"base64"})," encoded representation of your bytes. Centrifugo will decode the base64 back into bytes upon receipt and incorporate the result into the various places described above."]}),"\n",(0,i.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,i.jsxs)(n.p,{children:["This is an optional array of strings identifying the server-side channels to which the client will be subscribed. Further details can be found in the documentation on ",(0,i.jsx)(n.a,{href:"/docs/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["It's important to note that the ",(0,i.jsx)(n.code,{children:"channels"})," claim is sometimes ",(0,i.jsx)(n.strong,{children:"misinterpreted"})," by users as a list of channel permissions. It does not serve that purpose. Instead, using this claim causes the client to be automatically subscribed to the specified channels upon connection, making it unnecessary to invoke the ",(0,i.jsx)(n.code,{children:"subscribe"})," API from the client side. More information can be found in the ",(0,i.jsx)(n.a,{href:"/docs/server/server_subs",children:"server-side subscriptions"})," documentation."]})}),"\n",(0,i.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,i.jsxs)(n.p,{children:["This optional claim is a map of channels with options, providing a more detailed approach to server-side subscriptions compared to the ",(0,i.jsx)(n.code,{children:"channels"})," claim, as it allows for the annotation of each channel with additional information and data through options."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["The term ",(0,i.jsx)(n.code,{children:"subs"})," is shorthand for subscriptions. It should not be confused with the ",(0,i.jsx)(n.code,{children:"sub"})," claim mentioned earlier, which is a standard JWT claim used to provide a user ID (short for subject). Despite their similar names, these claims serve distinct purposes within a connection JWT."]})}),"\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:[(0,i.jsx)(n.code,{children:"meta"})," is an additional JSON object (e.g., ",(0,i.jsx)(n.code,{children:'{"key": "value"}'}),") that is attached to a connection. It differs from ",(0,i.jsx)(n.code,{children:"info"})," as it is never disclosed to clients within presence and join/leave events; it is only accessible on the server side. It can be included in proxy calls from Centrifugo to the application backend (refer to the ",(0,i.jsx)(n.code,{children:"proxy_include_connection_meta"})," option). In Centrifugo PRO, there is a ",(0,i.jsx)(n.code,{children:"connections"})," API method that returns this metadata within the connection description object."]}),"\n",(0,i.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,i.jsxs)(n.p,{children:["Although Centrifugo typically uses the ",(0,i.jsx)(n.code,{children:"exp"})," claim to manage connection expiration, there may be scenarios where you want to separate the token expiration check from the connection expiration time. When the ",(0,i.jsx)(n.code,{children:"expire_at"})," claim is included in the JWT, Centrifugo uses it to determine the connection expiration time, while the JWT expiration is still verified using the ",(0,i.jsx)(n.code,{children:"exp"})," claim."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp indicating when the connection should expire."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"To expire the connection at a specific future time, set it to that time."}),"\n",(0,i.jsxs)(n.li,{children:["To prevent connection expiration, set it to ",(0,i.jsx)(n.code,{children:"0"})," (token ",(0,i.jsx)(n.code,{children:"exp"})," claim will still be checked)."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,i.jsxs)(n.p,{children:["As mentioned, the ",(0,i.jsx)(n.code,{children:"exp"})," claim in a connection token is designed to expire the client connection at some point in time. Here's a detailed look at the process when Centrifugo identifies that the connection is going to expire."]}),"\n",(0,i.jsxs)(n.p,{children:["First, activate the client expiration mechanism in Centrifugo by providing a connection JWT with an ",(0,i.jsx)(n.code,{children:"exp"})," claim:"]}),"\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", algorithm="HS256")\n\nprint(token)\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Assuming the ",(0,i.jsx)(n.code,{children:"exp"})," claim is set to expire in 10 minutes, the client connects to Centrifugo with this token. Centrifugo will maintain the connection for the specified duration. Once the time elapses, Centrifugo allows a grace period (default is 25 seconds) for the client to refresh its credentials with a new valid token containing an updated ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Upon initial connection, the client receives a ",(0,i.jsx)(n.code,{children:"ttl"})," value in the connect response, indicating the seconds remaining before it must initiate a refresh command with new credentials. Centrifugo SDKs handle this ",(0,i.jsx)(n.code,{children:"ttl"})," internally and automatically begin the refresh process."]}),"\n",(0,i.jsxs)(n.p,{children:["SDKs provide mechanisms to hook into this process and provide a function to get new token. It's up to developer to decide how to load new token from the backend \u2013 in web browser this is usually a simple ",(0,i.jsx)(n.code,{children:"fetch"})," request and response may look like this:"]}),"\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:["You should provide the same connection JWT you issued when the page was initially rendered, but with an updated and valid ",(0,i.jsx)(n.code,{children:"exp"}),". Our SDKs will then send this token to the Centrifugo server, and the connection will be extended for the period set in the new ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"When you load new token from your app backend user authentication must be facilitated by your app's session mechanism. So you know for whom you are are going to generate an updated token."}),"\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","\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:"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: '42' })\n .setProtectedHeader({ alg })\n .sign(secret)\n\n console.log(token);\n})();\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 v3"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {\n token: token\n});\ncentrifuge.connect();\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See more details about working with connection tokens and handling token expiration on the client-side in the ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api#client-connection-token",children:"real-time SDK API spec"}),"."]}),"\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:"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: '42' })\n .setProtectedHeader({ alg })\n .setExpirationTime('5m')\n .sign(secret)\n\n console.log(token);\n})();\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:"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: '42', info: {\"name\": \"Alexander Emelin\"} })\n .setProtectedHeader({ alg })\n .setExpirationTime('5m')\n .sign(secret)\n\n console.log(token);\n})();\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.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Read a tutorial in our blog about ",(0,i.jsx)(n.a,{href:"/blog/2023/03/31/keycloak-sso-centrifugo",children:"using Centrifugo with Keycloak SSO"}),". In that case connection tokens are verified using public key loaded from the JWKS endpoint of Keycloak."]})}),"\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:["Centrifugo supports the following key types (",(0,i.jsx)(n.code,{children:"kty"}),") for JWKs tokens:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"RSA"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"EC"})," (since Centrifugo v5.1.0)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"OKP"})," based on Ed25519 (since Centrifugo v5.2.1)"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Once enabled JWKS used for both connection and channel subscription tokens."}),"\n",(0,i.jsx)(n.h2,{id:"dynamic-jwks-endpoint",children:"Dynamic JWKs endpoint"}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to extract variables from ",(0,i.jsx)(n.code,{children:"iss"})," and ",(0,i.jsx)(n.code,{children:"aud"})," JWT claims using ",(0,i.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"Go regexp"})," named groups, then use variables extracted during ",(0,i.jsx)(n.code,{children:"iss"})," or ",(0,i.jsx)(n.code,{children:"aud"})," matching to construct a JWKS endpoint dynamically upon token validation. In this case JWKS endpoint may be set in config as template."]}),"\n",(0,i.jsx)(n.p,{children:"To achieve this Centrifugo provides two additional options:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"token_issuer_regex"})," - match JWT issuer (",(0,i.jsx)(n.code,{children:"iss"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"token_audience_regex"})," - match JWT audience (",(0,i.jsx)(n.code,{children:"aud"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "token_issuer_regex": "https://example.com/auth/realms/(?P [A-z]+)",\n "token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To use variable in ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," it must be wrapped in ",(0,i.jsx)(n.code,{children:"{{"})," ",(0,i.jsx)(n.code,{children:"}}"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When using ",(0,i.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,i.jsx)(n.code,{children:"token_audience_regex"})," make sure ",(0,i.jsx)(n.code,{children:"token_issuer"})," and ",(0,i.jsx)(n.code,{children:"token_audience"})," not used in the config - otherwise and error will be returned on Centrifugo start."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,i.jsx)(n.code,{children:"token_audience_regex"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})})]})}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)}},54740:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/connection_token-ea4cfc0055be21bde9889325fc006a24.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/5de4a79c.e40f05d8.js b/assets/js/5de4a79c.e40f05d8.js deleted file mode 100644 index 6eec8ab6d..000000000 --- a/assets/js/5de4a79c.e40f05d8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8375],{62158:(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 JWT authentication"},c=void 0,l={id:"server/authentication",title:"Client JWT authentication",description:"To authenticate an incoming connection (client), Centrifugo can use a JSON Web Token (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism.",source:"@site/docs/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/authentication.md",tags:[],version:"current",frontMatter:{id:"authentication",title:"Client JWT authentication"},sidebar:"Guides",previous:{title:"Server API walkthrough",permalink:"/docs/server/server_api"},next:{title:"Channels and namespaces",permalink:"/docs/server/channels"}},d={},h=[{value:"Connection JWT Claims",id:"connection-jwt-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},{value:"Dynamic JWKs endpoint",id:"dynamic-jwks-endpoint",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 an incoming connection (client), Centrifugo can use a ",(0,i.jsx)(n.a,{href:"https://jwt.io/introduction",children:"JSON Web Token"})," (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["If you prefer not to use JWTs, consider the ",(0,i.jsx)(n.a,{href:"/docs/server/proxy",children:"proxy feature"}),". It enables the proxying of connection requests from Centrifugo to your application's backend endpoint for authentication."]})}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Using JWT for authentication can be beneficial in scenarios involving massive reconnects. As the authentication information is encoded in the token, this can significantly reduce the load on your application's session backend. For more details, refer to our ",(0,i.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"blog post"}),"."]})}),"\n",(0,i.jsx)(n.p,{children:"Upon connection, the client should supply a connection JWT containing several predefined credential claims. Below is a diagram illustrating this:"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{src:t(54740).Z+"",width:"2951",height:"1130"})}),"\n",(0,i.jsxs)(n.p,{children:["For more information about handling connection tokens on the client side, see the ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api#client-connection-token",children:"client SDK specification"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Currently, Centrifugo supports HMAC, RSA, and ECDSA JWT algorithms - specifically HS256, HS384, HS512, RSA256, RSA384, RSA512, EC256, EC384, and EC512."}),"\n",(0,i.jsxs)(n.p,{children:["Here, we will demonstrate example snippets using the Javascript Centrifugo client for the client-side and the ",(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 an HMAC secret key to Centrifugo, insert ",(0,i.jsx)(n.code,{children:"token_hmac_secret_key"})," into the 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": " "\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:"connection-jwt-claims",children:"Connection JWT Claims"}),"\n",(0,i.jsxs)(n.p,{children:["For connection JWT, Centrifugo uses some standard claims defined in ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"RFC 7519"}),", as well as custom Centrifugo-specific claims."]}),"\n",(0,i.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,i.jsxs)(n.p,{children:["This standard JWT claim must contain the ID of the current application user (",(0,i.jsx)(n.strong,{children:"as a string"}),")."]}),"\n",(0,i.jsxs)(n.p,{children:["If a user is not authenticated in the application but you wish to allow them to connect to Centrifugo, an empty string can be used as the user ID in the ",(0,i.jsx)(n.code,{children:"sub"})," claim. This facilitates anonymous access. In such cases, you might need to enable the corresponding channel namespace options that allow protocol features for anonymous users."]}),"\n",(0,i.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,i.jsx)(n.p,{children:"This claim specifies the UNIX timestamp (in seconds) when the token will expire. It is a standard JWT claim - all JWT libraries across different programming languages provide an API to set it."}),"\n",(0,i.jsxs)(n.p,{children:["If the ",(0,i.jsx)(n.code,{children:"exp"})," claim is not included, Centrifugo will not expire the connection. When included, a special algorithm will identify connections with an ",(0,i.jsx)(n.code,{children:"exp"})," in the past and initiate the connection refresh mechanism. The refresh mechanism allows a connection to be extended. If the refresh fails, Centrifugo will eventually close the client connection, which will not be accepted again until new valid and current credentials are provided in the connection token."]}),"\n",(0,i.jsx)(n.p,{children:"The connection expiration mechanism can be utilized in scenarios where you do not want users to remain subscribed to channels after being banned or deactivated in the application. It also serves to protect users from token leakage by setting a reasonably short expiration time."}),"\n",(0,i.jsxs)(n.p,{children:["Choose the ",(0,i.jsx)(n.code,{children:"exp"})," value judiciously; too short a value can lead to frequent application hits with refresh requests, whereas too long a value can result in delayed user connection deactivation. It's a matter of balance."]}),"\n",(0,i.jsxs)(n.p,{children:["Further details on connection expiration can be found ",(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 represents the UNIX time when the token was issued (in seconds). Refer to the ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6",children:"definition in RFC 7519"}),". This claim is optional but can be advantageous in conjunction with ",(0,i.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO's token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,i.jsxs)(n.p,{children:["This is a unique identifier for the token. Refer to the ",(0,i.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7",children:"definition in RFC 7519"}),". This claim is optional but can be beneficial in conjunction with ",(0,i.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO's token revocation features"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"aud",children:"aud"}),"\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/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})}),"\n",(0,i.jsx)(n.h3,{id:"iss",children:"iss"}),"\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/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})}),"\n",(0,i.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,i.jsx)(n.p,{children:"This optional claim provides additional information about the client's connection for Centrifugo. This information will be included in presence data, join/leave events, and client-side channel publications."}),"\n",(0,i.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,i.jsxs)(n.p,{children:["For those utilizing the binary Protobuf protocol and requiring the ",(0,i.jsx)(n.code,{children:"info"})," to be custom bytes, this field should be used."]}),"\n",(0,i.jsxs)(n.p,{children:["It contains a ",(0,i.jsx)(n.code,{children:"base64"})," encoded representation of your bytes. Centrifugo will decode the base64 back into bytes upon receipt and incorporate the result into the various places described above."]}),"\n",(0,i.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,i.jsxs)(n.p,{children:["This is an optional array of strings identifying the server-side channels to which the client will be subscribed. Further details can be found in the documentation on ",(0,i.jsx)(n.a,{href:"/docs/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["It's important to note that the ",(0,i.jsx)(n.code,{children:"channels"})," claim is sometimes ",(0,i.jsx)(n.strong,{children:"misinterpreted"})," by users as a list of channel permissions. It does not serve that purpose. Instead, using this claim causes the client to be automatically subscribed to the specified channels upon connection, making it unnecessary to invoke the ",(0,i.jsx)(n.code,{children:"subscribe"})," API from the client side. More information can be found in the ",(0,i.jsx)(n.a,{href:"/docs/server/server_subs",children:"server-side subscriptions"})," documentation."]})}),"\n",(0,i.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,i.jsxs)(n.p,{children:["This optional claim is a map of channels with options, providing a more detailed approach to server-side subscriptions compared to the ",(0,i.jsx)(n.code,{children:"channels"})," claim, as it allows for the annotation of each channel with additional information and data through options."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["The term ",(0,i.jsx)(n.code,{children:"subs"})," is shorthand for subscriptions. It should not be confused with the ",(0,i.jsx)(n.code,{children:"sub"})," claim mentioned earlier, which is a standard JWT claim used to provide a user ID (short for subject). Despite their similar names, these claims serve distinct purposes within a connection JWT."]})}),"\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:[(0,i.jsx)(n.code,{children:"meta"})," is an additional JSON object (e.g., ",(0,i.jsx)(n.code,{children:'{"key": "value"}'}),") that is attached to a connection. It differs from ",(0,i.jsx)(n.code,{children:"info"})," as it is never disclosed to clients within presence and join/leave events; it is only accessible on the server side. It can be included in proxy calls from Centrifugo to the application backend (refer to the ",(0,i.jsx)(n.code,{children:"proxy_include_connection_meta"})," option). In Centrifugo PRO, there is a ",(0,i.jsx)(n.code,{children:"connections"})," API method that returns this metadata within the connection description object."]}),"\n",(0,i.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,i.jsxs)(n.p,{children:["Although Centrifugo typically uses the ",(0,i.jsx)(n.code,{children:"exp"})," claim to manage connection expiration, there may be scenarios where you want to separate the token expiration check from the connection expiration time. When the ",(0,i.jsx)(n.code,{children:"expire_at"})," claim is included in the JWT, Centrifugo uses it to determine the connection expiration time, while the JWT expiration is still verified using the ",(0,i.jsx)(n.code,{children:"exp"})," claim."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp indicating when the connection should expire."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"To expire the connection at a specific future time, set it to that time."}),"\n",(0,i.jsxs)(n.li,{children:["To prevent connection expiration, set it to ",(0,i.jsx)(n.code,{children:"0"})," (token ",(0,i.jsx)(n.code,{children:"exp"})," claim will still be checked)."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,i.jsxs)(n.p,{children:["As mentioned, the ",(0,i.jsx)(n.code,{children:"exp"})," claim in a connection token is designed to expire the client connection at some point in time. Here's a detailed look at the process when Centrifugo identifies that the connection is going to expire."]}),"\n",(0,i.jsxs)(n.p,{children:["First, activate the client expiration mechanism in Centrifugo by providing a connection JWT with an ",(0,i.jsx)(n.code,{children:"exp"})," claim:"]}),"\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", algorithm="HS256")\n\nprint(token)\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Assuming the ",(0,i.jsx)(n.code,{children:"exp"})," claim is set to expire in 10 minutes, the client connects to Centrifugo with this token. Centrifugo will maintain the connection for the specified duration. Once the time elapses, Centrifugo allows a grace period (default is 25 seconds) for the client to refresh its credentials with a new valid token containing an updated ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Upon initial connection, the client receives a ",(0,i.jsx)(n.code,{children:"ttl"})," value in the connect response, indicating the seconds remaining before it must initiate a refresh command with new credentials. Centrifugo SDKs handle this ",(0,i.jsx)(n.code,{children:"ttl"})," internally and automatically begin the refresh process."]}),"\n",(0,i.jsxs)(n.p,{children:["SDKs provide mechanisms to hook into this process and provide a function to get new token. It's up to developer to decide how to load new token from the backend \u2013 in web browser this is usually a simple ",(0,i.jsx)(n.code,{children:"fetch"})," request and response may look like this:"]}),"\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:["You should provide the same connection JWT you issued when the page was initially rendered, but with an updated and valid ",(0,i.jsx)(n.code,{children:"exp"}),". Our SDKs will then send this token to the Centrifugo server, and the connection will be extended for the period set in the new ",(0,i.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"When you load new token from your app backend user authentication must be facilitated by your app's session mechanism. So you know for whom you are are going to generate an updated token."}),"\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","\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:"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: '42' })\n .setProtectedHeader({ alg })\n .sign(secret)\n\n console.log(token);\n})();\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 v3"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {\n token: token\n});\ncentrifuge.connect();\n'})}),"\n",(0,i.jsxs)(n.p,{children:["See more details about working with connection tokens and handling token expiration on the client-side in the ",(0,i.jsx)(n.a,{href:"/docs/transports/client_api#client-connection-token",children:"real-time SDK API spec"}),"."]}),"\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:"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: '42' })\n .setProtectedHeader({ alg })\n .setExpirationTime('5m')\n .sign(secret)\n\n console.log(token);\n})();\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:"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: '42', info: {\"name\": \"Alexander Emelin\"} })\n .setProtectedHeader({ alg })\n .setExpirationTime('5m')\n .sign(secret)\n\n console.log(token);\n})();\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.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:["Read a tutorial in our blog about ",(0,i.jsx)(n.a,{href:"/blog/2023/03/31/keycloak-sso-centrifugo",children:"using Centrifugo with Keycloak SSO"}),". In that case connection tokens are verified using public key loaded from the JWKS endpoint of Keycloak."]})}),"\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:["Centrifugo supports the following key types (",(0,i.jsx)(n.code,{children:"kty"}),") for JWKs tokens:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.code,{children:"RSA"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"EC"})," (since Centrifugo v5.1.0)"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Once enabled JWKS used for both connection and channel subscription tokens."}),"\n",(0,i.jsx)(n.h2,{id:"dynamic-jwks-endpoint",children:"Dynamic JWKs endpoint"}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to extract variables from ",(0,i.jsx)(n.code,{children:"iss"})," and ",(0,i.jsx)(n.code,{children:"aud"})," JWT claims using ",(0,i.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"Go regexp"})," named groups, then use variables extracted during ",(0,i.jsx)(n.code,{children:"iss"})," or ",(0,i.jsx)(n.code,{children:"aud"})," matching to construct a JWKS endpoint dynamically upon token validation. In this case JWKS endpoint may be set in config as template."]}),"\n",(0,i.jsx)(n.p,{children:"To achieve this Centrifugo provides two additional options:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"token_issuer_regex"})," - match JWT issuer (",(0,i.jsx)(n.code,{children:"iss"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"token_audience_regex"})," - match JWT audience (",(0,i.jsx)(n.code,{children:"aud"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "token_issuer_regex": "https://example.com/auth/realms/(?P [A-z]+)",\n "token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["To use variable in ",(0,i.jsx)(n.code,{children:"token_jwks_public_endpoint"})," it must be wrapped in ",(0,i.jsx)(n.code,{children:"{{"})," ",(0,i.jsx)(n.code,{children:"}}"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["When using ",(0,i.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,i.jsx)(n.code,{children:"token_audience_regex"})," make sure ",(0,i.jsx)(n.code,{children:"token_issuer"})," and ",(0,i.jsx)(n.code,{children:"token_audience"})," not used in the config - otherwise and error will be returned on Centrifugo start."]}),"\n",(0,i.jsx)(n.admonition,{type:"caution",children:(0,i.jsxs)(n.p,{children:["Setting ",(0,i.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,i.jsx)(n.code,{children:"token_audience_regex"})," will also affect subscription tokens (used for ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth",children:"channel token authorization"}),"). If you need to separate connection token configuration and subscription token configuration check out ",(0,i.jsx)(n.a,{href:"/docs/server/channel_token_auth#separate-subscription-token-config",children:"separate subscription token config"})," feature."]})})]})}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:()=>y});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 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 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 . 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 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 component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The 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 g=t(85893);function b(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,g.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,g.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,g.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function k(e){const n=j(e);return(0,g.jsxs)("div",{className:(0,s.Z)("tabs-container",m.tabList),children:[(0,g.jsx)(b,{...e,...n}),(0,g.jsx)(v,{...e,...n})]})}function y(e){const n=(0,f.Z)();return(0,g.jsx)(k,{...e,children:h(e.children)},String(n))}},54740:(e,n,t)=>{t.d(n,{Z:()=>i});const i=t.p+"assets/images/connection_token-ea4cfc0055be21bde9889325fc006a24.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/5e95c892.26146114.js b/assets/js/5e95c892.26146114.js deleted file mode 100644 index daac7b71d..000000000 --- a/assets/js/5e95c892.26146114.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9661],{41892:(e,r,s)=>{s.r(r),s.d(r,{default:()=>d});s(67294);var u=s(36905),a=s(10833),c=s(35281),n=s(18790),t=s(7372),l=s(85893);function d(e){return(0,l.jsx)(a.FG,{className:(0,u.Z)(c.k.wrapper.docsPages),children:(0,l.jsx)(t.Z,{children:(0,n.H)(e.route.routes)})})}}}]); \ No newline at end of file diff --git a/assets/js/5e95c892.4f6040e5.js b/assets/js/5e95c892.4f6040e5.js new file mode 100644 index 000000000..ece504bb6 --- /dev/null +++ b/assets/js/5e95c892.4f6040e5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9661],{93143:(e,r,s)=>{s.r(r),s.d(r,{default:()=>d});s(67294);var u=s(36905),a=s(44873),c=s(18015),n=s(18790),t=s(78299),l=s(85893);function d(e){return(0,l.jsx)(a.FG,{className:(0,u.Z)(c.k.wrapper.docsPages),children:(0,l.jsx)(t.Z,{children:(0,n.H)(e.route.routes)})})}}}]); \ No newline at end of file diff --git a/assets/js/6875c492.3506d76d.js b/assets/js/6875c492.3506d76d.js new file mode 100644 index 000000000..f52c8dc39 --- /dev/null +++ b/assets/js/6875c492.3506d76d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8610],{38555:(e,n,t)=>{t.d(n,{Z:()=>C});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(11614),o=t(18015);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 m(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 u(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:[(0,s.jsx)(m,{title:i,icon:t}),(0,s.jsx)(u,{children:a})]})}function g(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)(g,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.note",description:"The default label used for the Note admonition (:::note)",children:"note"})};function p(e){return(0,s.jsx)(h,{...x,...e,className:(0,l.Z)("alert alert--secondary",e.className),children:e.children})}function f(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)(f,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function v(e){return(0,s.jsx)(h,{...j,...e,className:(0,l.Z)("alert alert--success",e.className),children:e.children})}function b(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 N={icon:(0,s.jsx)(b,{}),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,{...N,...e,className:(0,l.Z)("alert alert--info",e.className),children:e.children})}function k(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 w={icon:(0,s.jsx)(k,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function _(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 P={icon:(0,s.jsx)(_,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const T={icon:(0,s.jsx)(k,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const I={...{note:p,tip:v,info:Z,warning:function(e){return(0,s.jsx)(h,{...w,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,s.jsx)(h,{...P,...e,className:(0,l.Z)("alert alert--danger",e.className),children:e.children})}},...{secondary:e=>(0,s.jsx)(p,{title:"secondary",...e}),important:e=>(0,s.jsx)(Z,{title:"important",...e}),success:e=>(0,s.jsx)(v,{title:"success",...e}),caution:function(e){return(0,s.jsx)(h,{...T,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})}}};function C(e){const n=a(e),t=(i=n.type,I[i]||(console.warn(`No admonition component found for admonition type "${i}". Using Info as fallback.`),I.info));var i;return(0,s.jsx)(t,{...n})}},38762:(e,n,t)=>{t.d(n,{Z:()=>v});var i=t(67294),s=t(36905),a=t(78299),l=t(94980),r=t(75013),o=t(11614),c=t(16550),d=t(18407);function m(e){const{pathname:n}=(0,c.TH)();return(0,i.useMemo)((()=>e.filter((e=>function(e,n){return!(e.unlisted&&!(0,d.Mg)(e.permalink,n))}(e,n)))),[e,n])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var h=t(85893);function g(e){let{sidebar:n}=e;const t=m(n.items);return(0,h.jsx)("aside",{className:"col col--3",children:(0,h.jsxs)("nav",{className:(0,s.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,h.jsx)("div",{className:(0,s.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:n.title}),(0,h.jsx)("ul",{className:(0,s.Z)(u.sidebarItemList,"clean-list"),children:t.map((e=>(0,h.jsx)("li",{className:u.sidebarItem,children:(0,h.jsx)(r.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var x=t(82306);function p(e){let{sidebar:n}=e;const t=m(n.items);return(0,h.jsx)("ul",{className:"menu__list",children:t.map((e=>(0,h.jsx)("li",{className:"menu__list-item",children:(0,h.jsx)(r.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function f(e){return(0,h.jsx)(x.Zo,{component:p,props:e})}function j(e){let{sidebar:n}=e;const t=(0,l.i)();return n?.items.length?"mobile"===t?(0,h.jsx)(f,{sidebar:n}):(0,h.jsx)(g,{sidebar:n}):null}function v(e){const{sidebar:n,toc:t,children:i,...l}=e,r=n&&n.items.length>0;return(0,h.jsx)(a.Z,{...l,children:(0,h.jsx)("div",{className:"container margin-vert--lg",children:(0,h.jsxs)("div",{className:"row",children:[(0,h.jsx)(j,{sidebar:n}),(0,h.jsx)("main",{className:(0,s.Z)("col",{"col--7":r,"col--9 col--offset-1":!r}),itemScope:!0,itemType:"https://schema.org/Blog",children:i}),t&&(0,h.jsx)("div",{className:"col col--2",children:t})]})})})}},61052:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(11614),s=t(16948),a=t(85893);function l(e){const{metadata:n}=e,{previousPage:t,nextPage:l}=n;return(0,a.jsxs)("nav",{className:"pagination-nav","aria-label":(0,i.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[t&&(0,a.jsx)(s.Z,{permalink:t,title:(0,a.jsx)(i.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),l&&(0,a.jsx)(s.Z,{permalink:l,title:(0,a.jsx)(i.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},83400:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(51402),s=t(17762),a=t(85893);function l(e){let{children:n,className:t}=e;const{frontMatter:l,assets:r,metadata:{description:o}}=(0,s.C)(),{withBaseUrl:c}=(0,i.C)(),d=r.image??l.image,m=l.keywords??[];return(0,a.jsxs)("article",{className:t,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,a.jsx)("meta",{itemProp:"description",content:o}),d&&(0,a.jsx)("link",{itemProp:"image",href:c(d,{absolute:!0})}),m.length>0&&(0,a.jsx)("meta",{itemProp:"keywords",content:m.join(",")}),n]})}},45462:(e,n,t)=>{t.r(n),t.d(n,{default:()=>v});t(67294);var i=t(36905),s=t(11614),a=t(57880),l=t(44873),r=t(18015),o=t(75013),c=t(38762),d=t(61052),m=t(26145),u=t(78969),h=t(94007),g=t(34055),x=t(85893);function p(e){const n=function(){const{selectMessage:e}=(0,a.c)();return n=>e(n,(0,s.I)({id:"theme.blog.post.plurals",description:'Pluralized label for "{count} posts". 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 post|{count} posts"},{count:n}))}();return(0,s.I)({id:"theme.blog.tagTitle",description:"The title of the page for a blog tag",message:'{nPosts} tagged with "{tagName}"'},{nPosts:n(e.count),tagName:e.label})}function f(e){let{tag:n}=e;const t=p(n);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(l.d,{title:t}),(0,x.jsx)(m.Z,{tag:"blog_tags_posts"})]})}function j(e){let{tag:n,items:t,sidebar:i,listMetadata:a}=e;const l=p(n);return(0,x.jsxs)(c.Z,{sidebar:i,children:[n.unlisted&&(0,x.jsx)(h.Z,{}),(0,x.jsxs)("header",{className:"margin-bottom--xl",children:[(0,x.jsx)(g.Z,{as:"h1",children:l}),(0,x.jsx)(o.Z,{href:n.allTagsPath,children:(0,x.jsx)(s.Z,{id:"theme.tags.tagsPageLink",description:"The label of the link targeting the tag list page",children:"View All Tags"})})]}),(0,x.jsx)(u.Z,{items:t}),(0,x.jsx)(d.Z,{metadata:a})]})}function v(e){return(0,x.jsxs)(l.FG,{className:(0,i.Z)(r.k.wrapper.blogPages,r.k.page.blogTagPostListPage),children:[(0,x.jsx)(f,{...e}),(0,x.jsx)(j,{...e})]})}},16948:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(36905),s=t(75013),a=t(85893);function l(e){const{permalink:n,title:t,subLabel:l,isNext:r}=e;return(0,a.jsxs)(s.Z,{className:(0,i.Z)("pagination-nav__link",r?"pagination-nav__link--next":"pagination-nav__link--prev"),to:n,children:[l&&(0,a.jsx)("div",{className:"pagination-nav__sublabel",children:l}),(0,a.jsx)("div",{className:"pagination-nav__label",children:t})]})}},94007:(e,n,t)=>{t.d(n,{Z:()=>h});t(67294);var i=t(36905),s=t(11614),a=t(32411),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(18015),m=t(38555);function u(e){let{className:n}=e;return(0,l.jsx)(m.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)(u,{...e})]})}},17762:(e,n,t)=>{t.d(n,{C:()=>o,n:()=>r});var i=t(67294),s=t(93478),a=t(85893);const l=i.createContext(null);function r(e){let{children:n,content:t,isBlogPostPage:s=!1}=e;const r=function(e){let{content:n,isBlogPostPage:t}=e;return(0,i.useMemo)((()=>({metadata:n.metadata,frontMatter:n.frontMatter,assets:n.assets,toc:n.toc,isBlogPostPage:t})),[n,t])}({content:t,isBlogPostPage:s});return(0,a.jsx)(l.Provider,{value:r,children:n})}function o(){const e=(0,i.useContext)(l);if(null===e)throw new s.i6("BlogPostProvider");return e}},57880:(e,n,t)=>{t.d(n,{c:()=>c});var i=t(67294),s=t(6832);const a=["zero","one","two","few","many","other"];function l(e){return a.filter((n=>e.includes(n)))}const r={locale:"en",pluralForms:l(["one","other"]),select:e=>1===e?"one":"other"};function o(){const{i18n:{currentLocale:e}}=(0,s.Z)();return(0,i.useMemo)((()=>{try{return function(e){const n=new Intl.PluralRules(e);return{locale:e,pluralForms:l(n.resolvedOptions().pluralCategories),select:e=>n.select(e)}}(e)}catch(n){return console.error(`Failed to use Intl.PluralRules for locale "${e}".\nDocusaurus will fallback to the default (English) implementation.\nError: ${n.message}\n`),r}}),[e])}function c(){const e=o();return{selectMessage:(n,t)=>function(e,n,t){const i=e.split("|");if(1===i.length)return i[0];i.length>t.pluralForms.length&&console.error(`For locale=${t.locale}, a maximum of ${t.pluralForms.length} plural forms are expected (${t.pluralForms.join(",")}), but the message contains ${i.length}: ${e}`);const s=t.select(n),a=t.pluralForms.indexOf(s);return i[Math.min(a,i.length-1)]}(t,n,e)}}},78969:(e,n,t)=>{t.d(n,{Z:()=>o});t(67294);var i=t(75013),s=t(17762),a=t(83400);const l={container:"container_nU41",leftColumn:"leftColumn_mxRM"};var r=t(85893);function o(e){return(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(c,{...e})})}function c(e){let{items:n,component:t=d}=e;return(0,r.jsx)(r.Fragment,{children:n.map((e=>{let{content:n}=e;return(0,r.jsx)(s.n,{content:n,children:(0,r.jsx)(t,{children:(0,r.jsx)(n,{})})},n.metadata.permalink)}))})}function d(e){let{className:n}=e;const{metadata:t}=(0,s.C)(),{permalink:o,title:c,formattedDate:d,frontMatter:m,description:u}=t,h=t.authors[0];return(0,r.jsx)(a.Z,{className:n,children:(0,r.jsxs)("div",{className:l.container,children:[(0,r.jsx)("div",{className:l.leftColumn,children:(0,r.jsx)("img",{src:m.image,width:"200px"})}),(0,r.jsxs)("div",{className:l.rightColumn,children:[(0,r.jsx)("div",{children:(0,r.jsx)(i.Z,{itemProp:"url",to:o,style:{fontSize:"1.0em"},children:c})}),(0,r.jsxs)("div",{style:{fontSize:"0.8em",color:"#6d6666"},children:[d," by ",h?.name]}),(0,r.jsx)("div",{children:(0,r.jsx)("div",{children:(0,r.jsx)("div",{style:{fontSize:"0.9em"},children:u})})})]})]})})}}}]); \ No newline at end of file diff --git a/assets/js/6875c492.5968119b.js b/assets/js/6875c492.5968119b.js deleted file mode 100644 index 0fb914be4..000000000 --- a/assets/js/6875c492.5968119b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8610],{59047:(e,n,t)=>{t.d(n,{Z:()=>C});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 m(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 u(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:[(0,s.jsx)(m,{title:i,icon:t}),(0,s.jsx)(u,{children:a})]})}function g(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)(g,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.note",description:"The default label used for the Note admonition (:::note)",children:"note"})};function p(e){return(0,s.jsx)(h,{...x,...e,className:(0,l.Z)("alert alert--secondary",e.className),children:e.children})}function f(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)(f,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function v(e){return(0,s.jsx)(h,{...j,...e,className:(0,l.Z)("alert alert--success",e.className),children:e.children})}function b(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 N={icon:(0,s.jsx)(b,{}),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,{...N,...e,className:(0,l.Z)("alert alert--info",e.className),children:e.children})}function k(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 w={icon:(0,s.jsx)(k,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function _(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 P={icon:(0,s.jsx)(_,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const T={icon:(0,s.jsx)(k,{}),title:(0,s.jsx)(r.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const I={...{note:p,tip:v,info:Z,warning:function(e){return(0,s.jsx)(h,{...w,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,s.jsx)(h,{...P,...e,className:(0,l.Z)("alert alert--danger",e.className),children:e.children})}},...{secondary:e=>(0,s.jsx)(p,{title:"secondary",...e}),important:e=>(0,s.jsx)(Z,{title:"important",...e}),success:e=>(0,s.jsx)(v,{title:"success",...e}),caution:function(e){return(0,s.jsx)(h,{...T,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})}}};function C(e){const n=a(e),t=(i=n.type,I[i]||(console.warn(`No admonition component found for admonition type "${i}". Using Info as fallback.`),I.info));var i;return(0,s.jsx)(t,{...n})}},61460:(e,n,t)=>{t.d(n,{Z:()=>v});var i=t(67294),s=t(36905),a=t(7372),l=t(87524),r=t(39960),o=t(95999),c=t(16550),d=t(48596);function m(e){const{pathname:n}=(0,c.TH)();return(0,i.useMemo)((()=>e.filter((e=>function(e,n){return!(e.unlisted&&!(0,d.Mg)(e.permalink,n))}(e,n)))),[e,n])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var h=t(85893);function g(e){let{sidebar:n}=e;const t=m(n.items);return(0,h.jsx)("aside",{className:"col col--3",children:(0,h.jsxs)("nav",{className:(0,s.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,h.jsx)("div",{className:(0,s.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:n.title}),(0,h.jsx)("ul",{className:(0,s.Z)(u.sidebarItemList,"clean-list"),children:t.map((e=>(0,h.jsx)("li",{className:u.sidebarItem,children:(0,h.jsx)(r.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var x=t(13102);function p(e){let{sidebar:n}=e;const t=m(n.items);return(0,h.jsx)("ul",{className:"menu__list",children:t.map((e=>(0,h.jsx)("li",{className:"menu__list-item",children:(0,h.jsx)(r.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function f(e){return(0,h.jsx)(x.Zo,{component:p,props:e})}function j(e){let{sidebar:n}=e;const t=(0,l.i)();return n?.items.length?"mobile"===t?(0,h.jsx)(f,{sidebar:n}):(0,h.jsx)(g,{sidebar:n}):null}function v(e){const{sidebar:n,toc:t,children:i,...l}=e,r=n&&n.items.length>0;return(0,h.jsx)(a.Z,{...l,children:(0,h.jsx)("div",{className:"container margin-vert--lg",children:(0,h.jsxs)("div",{className:"row",children:[(0,h.jsx)(j,{sidebar:n}),(0,h.jsx)("main",{className:(0,s.Z)("col",{"col--7":r,"col--9 col--offset-1":!r}),itemScope:!0,itemType:"https://schema.org/Blog",children:i}),t&&(0,h.jsx)("div",{className:"col col--2",children:t})]})})})}},99703:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(95999),s=t(32244),a=t(85893);function l(e){const{metadata:n}=e,{previousPage:t,nextPage:l}=n;return(0,a.jsxs)("nav",{className:"pagination-nav","aria-label":(0,i.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[t&&(0,a.jsx)(s.Z,{permalink:t,title:(0,a.jsx)(i.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),l&&(0,a.jsx)(s.Z,{permalink:l,title:(0,a.jsx)(i.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},15289:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(44996),s=t(9460),a=t(85893);function l(e){let{children:n,className:t}=e;const{frontMatter:l,assets:r,metadata:{description:o}}=(0,s.C)(),{withBaseUrl:c}=(0,i.C)(),d=r.image??l.image,m=l.keywords??[];return(0,a.jsxs)("article",{className:t,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,a.jsx)("meta",{itemProp:"description",content:o}),d&&(0,a.jsx)("link",{itemProp:"image",href:c(d,{absolute:!0})}),m.length>0&&(0,a.jsx)("meta",{itemProp:"keywords",content:m.join(",")}),n]})}},41714:(e,n,t)=>{t.r(n),t.d(n,{default:()=>v});t(67294);var i=t(36905),s=t(95999),a=t(88824),l=t(10833),r=t(35281),o=t(39960),c=t(61460),d=t(99703),m=t(90197),u=t(42426),h=t(22212),g=t(92503),x=t(85893);function p(e){const n=function(){const{selectMessage:e}=(0,a.c)();return n=>e(n,(0,s.I)({id:"theme.blog.post.plurals",description:'Pluralized label for "{count} posts". 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 post|{count} posts"},{count:n}))}();return(0,s.I)({id:"theme.blog.tagTitle",description:"The title of the page for a blog tag",message:'{nPosts} tagged with "{tagName}"'},{nPosts:n(e.count),tagName:e.label})}function f(e){let{tag:n}=e;const t=p(n);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(l.d,{title:t}),(0,x.jsx)(m.Z,{tag:"blog_tags_posts"})]})}function j(e){let{tag:n,items:t,sidebar:i,listMetadata:a}=e;const l=p(n);return(0,x.jsxs)(c.Z,{sidebar:i,children:[n.unlisted&&(0,x.jsx)(h.Z,{}),(0,x.jsxs)("header",{className:"margin-bottom--xl",children:[(0,x.jsx)(g.Z,{as:"h1",children:l}),(0,x.jsx)(o.Z,{href:n.allTagsPath,children:(0,x.jsx)(s.Z,{id:"theme.tags.tagsPageLink",description:"The label of the link targeting the tag list page",children:"View All Tags"})})]}),(0,x.jsx)(u.Z,{items:t}),(0,x.jsx)(d.Z,{metadata:a})]})}function v(e){return(0,x.jsxs)(l.FG,{className:(0,i.Z)(r.k.wrapper.blogPages,r.k.page.blogTagPostListPage),children:[(0,x.jsx)(f,{...e}),(0,x.jsx)(j,{...e})]})}},32244:(e,n,t)=>{t.d(n,{Z:()=>l});t(67294);var i=t(36905),s=t(39960),a=t(85893);function l(e){const{permalink:n,title:t,subLabel:l,isNext:r}=e;return(0,a.jsxs)(s.Z,{className:(0,i.Z)("pagination-nav__link",r?"pagination-nav__link--next":"pagination-nav__link--prev"),to:n,children:[l&&(0,a.jsx)("div",{className:"pagination-nav__sublabel",children:l}),(0,a.jsx)("div",{className:"pagination-nav__label",children:t})]})}},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),m=t(59047);function u(e){let{className:n}=e;return(0,l.jsx)(m.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)(u,{...e})]})}},9460:(e,n,t)=>{t.d(n,{C:()=>o,n:()=>r});var i=t(67294),s=t(902),a=t(85893);const l=i.createContext(null);function r(e){let{children:n,content:t,isBlogPostPage:s=!1}=e;const r=function(e){let{content:n,isBlogPostPage:t}=e;return(0,i.useMemo)((()=>({metadata:n.metadata,frontMatter:n.frontMatter,assets:n.assets,toc:n.toc,isBlogPostPage:t})),[n,t])}({content:t,isBlogPostPage:s});return(0,a.jsx)(l.Provider,{value:r,children:n})}function o(){const e=(0,i.useContext)(l);if(null===e)throw new s.i6("BlogPostProvider");return e}},88824:(e,n,t)=>{t.d(n,{c:()=>c});var i=t(67294),s=t(52263);const a=["zero","one","two","few","many","other"];function l(e){return a.filter((n=>e.includes(n)))}const r={locale:"en",pluralForms:l(["one","other"]),select:e=>1===e?"one":"other"};function o(){const{i18n:{currentLocale:e}}=(0,s.Z)();return(0,i.useMemo)((()=>{try{return function(e){const n=new Intl.PluralRules(e);return{locale:e,pluralForms:l(n.resolvedOptions().pluralCategories),select:e=>n.select(e)}}(e)}catch(n){return console.error(`Failed to use Intl.PluralRules for locale "${e}".\nDocusaurus will fallback to the default (English) implementation.\nError: ${n.message}\n`),r}}),[e])}function c(){const e=o();return{selectMessage:(n,t)=>function(e,n,t){const i=e.split("|");if(1===i.length)return i[0];i.length>t.pluralForms.length&&console.error(`For locale=${t.locale}, a maximum of ${t.pluralForms.length} plural forms are expected (${t.pluralForms.join(",")}), but the message contains ${i.length}: ${e}`);const s=t.select(n),a=t.pluralForms.indexOf(s);return i[Math.min(a,i.length-1)]}(t,n,e)}}},42426:(e,n,t)=>{t.d(n,{Z:()=>o});t(67294);var i=t(39960),s=t(9460),a=t(15289);const l={container:"container_nU41",leftColumn:"leftColumn_mxRM"};var r=t(85893);function o(e){return(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(c,{...e})})}function c(e){let{items:n,component:t=d}=e;return(0,r.jsx)(r.Fragment,{children:n.map((e=>{let{content:n}=e;return(0,r.jsx)(s.n,{content:n,children:(0,r.jsx)(t,{children:(0,r.jsx)(n,{})})},n.metadata.permalink)}))})}function d(e){let{className:n}=e;const{metadata:t}=(0,s.C)(),{permalink:o,title:c,formattedDate:d,frontMatter:m,description:u}=t,h=t.authors[0];return(0,r.jsx)(a.Z,{className:n,children:(0,r.jsxs)("div",{className:l.container,children:[(0,r.jsx)("div",{className:l.leftColumn,children:(0,r.jsx)("img",{src:m.image,width:"200px"})}),(0,r.jsxs)("div",{className:l.rightColumn,children:[(0,r.jsx)("div",{children:(0,r.jsx)(i.Z,{itemProp:"url",to:o,style:{fontSize:"1.0em"},children:c})}),(0,r.jsxs)("div",{style:{fontSize:"0.8em",color:"#6d6666"},children:[d," by ",h?.name]}),(0,r.jsx)("div",{children:(0,r.jsx)("div",{children:(0,r.jsx)("div",{style:{fontSize:"0.9em"},children:u})})})]})]})})}}}]); \ No newline at end of file diff --git a/assets/js/6fbe284c.16af36fa.js b/assets/js/6fbe284c.16af36fa.js deleted file mode 100644 index 4816a064a..000000000 --- a/assets/js/6fbe284c.16af36fa.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7572],{92772:(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/docs/pro/user_status.md",sourceDirName:"pro",slug:"/pro/user_status",permalink:"/docs/pro/user_status",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/user_status.md",tags:[],version:"current",frontMatter:{id:"user_status",title:"User status API"},sidebar:"Pro",previous:{title:"Push notification API",permalink:"/docs/pro/push_notifications"},next:{title:"Connections API",permalink:"/docs/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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/update_user_status\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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/get_user_status\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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/delete_user_status\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/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 diff --git a/assets/js/6fbe284c.39e4d39a.js b/assets/js/6fbe284c.39e4d39a.js new file mode 100644 index 000000000..c5c00d6b3 --- /dev/null +++ b/assets/js/6fbe284c.39e4d39a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7572],{92772:(e,s,t)=>{t.r(s),t.d(s,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>i,metadata:()=>l,toc:()=>u});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/docs/pro/user_status.md",sourceDirName:"pro",slug:"/pro/user_status",permalink:"/docs/pro/user_status",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/user_status.md",tags:[],version:"current",frontMatter:{id:"user_status",title:"User status API"},sidebar:"Pro",previous:{title:"SSO for admin UI (OIDC)",permalink:"/docs/pro/admin_idp_auth"},next:{title:"Connections API",permalink:"/docs/pro/connections"}},d={},u=[{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 o(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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/update_user_status\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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/get_user_status\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 "X-API-Key: " \\\n --request POST \\\n --data \'{"users": ["42"]}\' \\\n http://localhost:8000/api/delete_user_status\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/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)(o,{...e})}):o(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 diff --git a/assets/js/7264.215321c8.js b/assets/js/7264.215321c8.js new file mode 100644 index 000000000..f702fe03e --- /dev/null +++ b/assets/js/7264.215321c8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7264],{38555:(e,n,t)=>{t.d(n,{Z:()=>w});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),o=t(11614),r=t(18015);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)(r.k.common.admonition,r.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:[(0,s.jsx)(u,{title:i,icon:t}),(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)(o.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 g(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)(g,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function p(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 C={icon:(0,s.jsx)(N,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.info",description:"The default label used for the Info admonition (:::info)",children:"info"})};function L(e){return(0,s.jsx)(h,{...C,...e,className:(0,l.Z)("alert alert--info",e.className),children:e.children})}function b(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 Z={icon:(0,s.jsx)(b,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function H(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 y={icon:(0,s.jsx)(H,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const k={icon:(0,s.jsx)(b,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const _={...{note:v,tip:p,info:L,warning:function(e){return(0,s.jsx)(h,{...Z,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,s.jsx)(h,{...y,...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)(L,{title:"important",...e}),success:e=>(0,s.jsx)(p,{title:"success",...e}),caution:function(e){return(0,s.jsx)(h,{...k,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})}}};function w(e){const n=a(e),t=(i=n.type,_[i]||(console.warn(`No admonition component found for admonition type "${i}". Using Info as fallback.`),_.info));var i;return(0,s.jsx)(t,{...n})}},48480:(e,n,t)=>{t.d(n,{Z:()=>w});var i=t(67294),s=t(11151),a=t(32411),l=t(84316),o=t(85893);function r(e){return(0,o.jsx)("code",{...e})}var c=t(75013);var d=t(36905),u=t(788),m=t(5730),h=t(17940);const f={details:"details_lb9f",isBrowser:"isBrowser_bmU9",collapsibleContent:"collapsibleContent_i85q"};function x(e){return!!e&&("SUMMARY"===e.tagName||x(e.parentElement))}function v(e,n){return!!e&&(e===n||v(e.parentElement,n))}function g(e){let{summary:n,children:t,...s}=e;const a=(0,m.Z)(),l=(0,i.useRef)(null),{collapsed:r,setCollapsed:c}=(0,h.u)({initialState:!s.open}),[d,g]=(0,i.useState)(s.open),j=i.isValidElement(n)?n:(0,o.jsx)("summary",{children:n??"Details"});return(0,o.jsxs)("details",{...s,ref:l,open:d,"data-collapsed":r,className:(0,u.Z)(f.details,a&&f.isBrowser,s.className),onMouseDown:e=>{x(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();const n=e.target;x(n)&&v(n,l.current)&&(e.preventDefault(),r?(c(!1),g(!0)):c(!0))},children:[j,(0,o.jsx)(h.z,{lazy:!1,collapsed:r,disableSSRStyle:!0,onCollapseTransitionEnd:e=>{c(e),g(!e)},children:(0,o.jsx)("div",{className:f.collapsibleContent,children:t})})]})}const j={details:"details_b_Ee"},p="alert alert--info";function N(e){let{...n}=e;return(0,o.jsx)(g,{...n,className:(0,d.Z)(p,j.details,n.className)})}function C(e){const n=i.Children.toArray(e.children),t=n.find((e=>i.isValidElement(e)&&"summary"===e.type)),s=(0,o.jsx)(o.Fragment,{children:n.filter((e=>e!==t))});return(0,o.jsx)(N,{...e,summary:t,children:s})}var L=t(34055);function b(e){return(0,o.jsx)(L.Z,{...e})}const Z={containsTaskList:"containsTaskList_mC6p"};function H(e){if(void 0!==e)return(0,d.Z)(e,e?.includes("contains-task-list")&&Z.containsTaskList)}const y={img:"img_ev3q"};var k=t(38555);const _={Head:a.Z,details:C,Details:C,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,o.jsx)(r,{...e}):(0,o.jsx)(l.Z,{...e})},a:function(e){return(0,o.jsx)(c.Z,{...e})},pre:function(e){return(0,o.jsx)(o.Fragment,{children:e.children})},ul:function(e){return(0,o.jsx)("ul",{...e,className:H(e.className)})},img:function(e){return(0,o.jsx)("img",{loading:"lazy",...e,className:(n=e.className,(0,d.Z)(n,y.img))});var n},h1:e=>(0,o.jsx)(b,{as:"h1",...e}),h2:e=>(0,o.jsx)(b,{as:"h2",...e}),h3:e=>(0,o.jsx)(b,{as:"h3",...e}),h4:e=>(0,o.jsx)(b,{as:"h4",...e}),h5:e=>(0,o.jsx)(b,{as:"h5",...e}),h6:e=>(0,o.jsx)(b,{as:"h6",...e}),admonition:k.Z,mermaid:()=>null};function w(e){let{children:n}=e;return(0,o.jsx)(s.Z,{components:_,children:n})}},95967:(e,n,t)=>{t.d(n,{Z:()=>c});t(67294);var i=t(36905),s=t(21351);const a={tableOfContents:"tableOfContents_bqdL",docItemContainer:"docItemContainer_F8PC"};var l=t(85893);const o="table-of-contents__link toc-highlight",r="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:o,linkActiveClassName:r})})}},21351:(e,n,t)=>{t.d(n,{Z:()=>x});var i=t(67294),s=t(96793);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 o(e){const n=e.getBoundingClientRect();return n.top===n.bottom?o(e.parentNode):n}function r(e,n){let{anchorTopOffset:t}=n;const i=e.find((e=>o(e).top>=t));if(i){return function(e){return e.top>0&&e.bottom {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 o(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(i),o=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=r(o,{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",o),document.addEventListener("resize",o),o(),()=>{document.removeEventListener("scroll",o),document.removeEventListener("resize",o)}}),[e,t])}var u=t(75013),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:o="table-of-contents__link",linkActiveClassName:r,minHeadingLevel:c,maxHeadingLevel:u,...h}=e;const x=(0,s.L)(),v=c??x.tableOfContents.minHeadingLevel,g=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:g});return d((0,i.useMemo)((()=>{if(o&&r)return{linkClassName:o,linkActiveClassName:r,minHeadingLevel:v,maxHeadingLevel:g}}),[o,r,v,g])),(0,m.jsx)(f,{toc:j,className:t,linkClassName:o,...h})}},94007:(e,n,t)=>{t.d(n,{Z:()=>h});t(67294);var i=t(36905),s=t(11614),a=t(32411),l=t(85893);function o(){return(0,l.jsx)(s.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function r(){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(18015),u=t(38555);function m(e){let{className:n}=e;return(0,l.jsx)(u.Z,{type:"caution",title:(0,l.jsx)(o,{}),className:(0,i.Z)(n,d.k.common.unlistedBanner),children:(0,l.jsx)(r,{})})}function h(e){return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c,{}),(0,l.jsx)(m,{...e})]})}}}]); \ No newline at end of file diff --git a/assets/js/7672fb2a.3eb525cd.js b/assets/js/7672fb2a.3eb525cd.js new file mode 100644 index 000000000..38c18d17e --- /dev/null +++ b/assets/js/7672fb2a.3eb525cd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8589],{30433:(e,n,i)=>{i.d(n,{Z:()=>r});i(67294);var t=i(36905);const s={tabItem:"tabItem_Ymn6"};var o=i(85893);function r(e){let{children:n,hidden:i,className:r}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,t.Z)(s.tabItem,r),hidden:i,children:n})}},22808:(e,n,i)=>{i.d(n,{Z:()=>k});var t=i(67294),s=i(36905),o=i(63735),r=i(16550),a=i(20613),c=i(34423),l=i(20636),d=i(99200);function h(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.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 u(e){const{values:n,children:i}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:i,attributes:t,default:s}}=e;return{value:n,label:i,attributes:t,default:s}}))}(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 x(e){let{queryString:n=!1,groupId:i}=e;const s=(0,r.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,t.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:i=!1,groupId:s}=e,o=u(e),[r,c]=(0,t.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 t=i.find((e=>e.default))??i[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:o}))),[l,h]=x({queryString:i,groupId:s}),[j,f]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,o]=(0,d.Nk)(i);return[s,(0,t.useCallback)((e=>{i&&o.set(e)}),[i,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,t.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=i(5730);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=i(85893);function g(e){let{className:n,block:i,selectedValue:t,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),d=e=>{const n=e.currentTarget,i=c.indexOf(n),s=a[i].value;s!==t&&(l(n),r(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(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,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":i},n),children:a.map((e=>{let{value:n,label:i,attributes:o}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...o,className:(0,s.Z)("tabs__item",m.tabItem,o?.className,{"tabs__item--active":t===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:s}=e;const o=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,t.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,{...e,...n}),(0,b.jsx)(v,{...e,...n})]})}function k(e){const n=(0,f.Z)();return(0,b.jsx)(y,{...e,children:h(e.children)},String(n))}},99512:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var t=i(85893),s=i(11151),o=i(22808),r=i(30433);const a={id:"authentication",title:"Client JWT authentication"},c=void 0,l={id:"server/authentication",title:"Client JWT 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-4/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/4/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/server/authentication.md",tags:[],version:"4",frontMatter:{id:"authentication",title:"Client JWT authentication"},sidebar:"Guides",previous:{title:"Server API walkthrough",permalink:"/docs/4/server/server_api"},next:{title:"Channels and namespaces",permalink:"/docs/4/server/channels"}},d={},h=[{value:"Connection JWT claims",id:"connection-jwt-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},{value:"Dynamic JWKs endpoint",id:"dynamic-jwks-endpoint",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,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["To authenticate incoming connection (client) Centrifugo can use ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["If you prefer to avoid using JWT then look at ",(0,t.jsx)(n.a,{href:"/docs/4/server/proxy",children:"the proxy feature"}),". It allows proxying connection requests from Centrifugo to your application backend endpoint for authentication details."]})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["Using JWT auth can be nice in terms of massive reconnect scenario. Since authentication information is encoded directly in the token this may help to drastically reduce load on your application session backend. See in our ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"blog post"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"Upon connecting to Centrifugo client should provide a connection JWT with several predefined credential claims. Here is a diagram:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(32691).Z+"",width:"2600",height:"906"})}),"\n",(0,t.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,t.jsxs)(n.p,{children:["We will use Javascript Centrifugo client here for example snippets for client-side and ",(0,t.jsx)(n.a,{href:"https://github.com/jpadilla/pyjwt",children:"PyJWT"})," Python library to generate a connection token on the backend side."]}),"\n",(0,t.jsxs)(n.p,{children:["To add HMAC secret key to Centrifugo add ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," to configuration file:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_hmac_secret_key": " "\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["To add RSA public key (must be PEM encoded string) add ",(0,t.jsx)(n.code,{children:"token_rsa_public_key"})," option, ex:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["To add ECDSA public key (must be PEM encoded string) add ",(0,t.jsx)(n.code,{children:"token_ecdsa_public_key"})," option, ex:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h2,{id:"connection-jwt-claims",children:"Connection JWT claims"}),"\n",(0,t.jsxs)(n.p,{children:["For connection JWT Centrifugo uses the some standart claims defined in ",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"rfc7519"}),", also some custom Centrifugo-specific."]}),"\n",(0,t.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,t.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,t.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,t.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,t.jsx)(n.code,{children:"sub"})," claim. This is called anonymous access. In this case, you may need to enable corresponding channel namespace options which enable access to protocol features for anonymous users."]}),"\n",(0,t.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,t.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,t.jsxs)(n.p,{children:["If ",(0,t.jsx)(n.code,{children:"exp"})," claim is not provided then Centrifugo won't expire connection. When provided special algorithm will find connections with ",(0,t.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,t.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,t.jsxs)(n.p,{children:["Choose ",(0,t.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,t.jsxs)(n.p,{children:["Read more about connection expiration ",(0,t.jsx)(n.a,{href:"#connection-expiration",children:"below"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,t.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,t.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,t.jsx)(n.a,{href:"/docs/4/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,t.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,t.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,t.jsx)(n.a,{href:"/docs/4/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim)."]}),"\n",(0,t.jsxs)(n.p,{children:["But you can force this check by setting ",(0,t.jsx)(n.code,{children:"token_audience"})," string option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_audience": "centrifugo"\n}\n'})}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_audience"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})}),"\n",(0,t.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim)."]}),"\n",(0,t.jsxs)(n.p,{children:["But you can force this check by setting ",(0,t.jsx)(n.code,{children:"token_issuer"})," string option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_issuer": "my_app"\n}\n'})}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_issuer"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})}),"\n",(0,t.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,t.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,t.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,t.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,t.jsxs)(n.p,{children:["This field contains a ",(0,t.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,t.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,t.jsxs)(n.p,{children:["An optional array of strings with server-side channels to subscribe a client to. See more details about ",(0,t.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,t.jsxs)(n.p,{children:["An optional map of channels with options. This is like a ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["This claim is called ",(0,t.jsx)(n.code,{children:"subs"})," as a shortcut from subscriptions. The claim ",(0,t.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,t.jsx)(n.p,{children:"Example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h4,{id:"subscribe-options",children:"Subscribe options:"}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"info"}),(0,t.jsx)(n.td,{children:"JSON object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom channel info"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"b64info"}),(0,t.jsx)(n.td,{children:"string"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom channel info in Base64 - to pass binary channel info"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"data"}),(0,t.jsx)(n.td,{children:"JSON object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom JSON data to return in subscription context inside Connect reply"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"b64data"}),(0,t.jsx)(n.td,{children:"string"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["Same as ",(0,t.jsx)(n.code,{children:"data"})," but in Base64 to send binary data"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"override"}),(0,t.jsx)(n.td,{children:"Override object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.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,t.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"presence"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override presence"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override join_leave"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"position"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override position"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"recover"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"meta",children:"meta"}),"\n",(0,t.jsxs)(n.p,{children:["Meta is an additional JSON object (ex. ",(0,t.jsx)(n.code,{children:'{"key": "value"}'}),") that will be attached to a connection. Unlike ",(0,t.jsx)(n.code,{children:"info"})," it's never exposed to clients inside presence and join/leave payloads and only accessible on a backend side. It may be included in proxy calls from Centrifugo to the application backend (see ",(0,t.jsx)(n.code,{children:"proxy_include_connection_meta"})," option). Also, there is a ",(0,t.jsx)(n.code,{children:"connections"})," API method in Centrifugo PRO that returns this data in the connection description object."]}),"\n",(0,t.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo looks on ",(0,t.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,t.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,t.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the connection should expire."]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Set it to the future time for expiring connection at some point"}),"\n",(0,t.jsxs)(n.li,{children:["Set it to ",(0,t.jsx)(n.code,{children:"0"})," to disable connection expiration (but still check token ",(0,t.jsx)(n.code,{children:"exp"})," claim)."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,t.jsxs)(n.p,{children:["As said above ",(0,t.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,t.jsx)(n.p,{children:"First, you should do is enable client expiration mechanism in Centrifugo providing a connection JWT with expiration:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["Let's suppose that you set ",(0,t.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,t.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["When a client first connects to Centrifugo it receives the ",(0,t.jsx)(n.code,{children:"ttl"})," value in connect reply. That ",(0,t.jsx)(n.code,{children:"ttl"})," value contains the number of seconds after which the client must send the ",(0,t.jsx)(n.code,{children:"refresh"})," command with new credentials to Centrifugo. Centrifugo clients must handle this ",(0,t.jsx)(n.code,{children:"ttl"})," field and automatically start the refresh process."]}),"\n",(0,t.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,t.jsx)(n.code,{children:"/centrifuge/refresh"})," URL endpoint. In response your server must return JSON with a new connection JWT:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'{\n "token": token\n}\n'})}),"\n",(0,t.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,t.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,t.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,t.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,t.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,t.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,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.p,{children:"Let's look at how to generate connection HS256 JWT in Python:"}),"\n",(0,t.jsx)(n.h3,{id:"simplest-token",children:"Simplest token"}),"\n","\n","\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'import jwt\n\ntoken = jwt.encode({"sub": "42"}, "secret").decode()\n\nprint(token)\n'})})}),(0,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["Note that we use the value of ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo config here (in this case ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," value is just ",(0,t.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,t.jsx)(n.p,{children:"Then you can pass this token to your client side and use it when connecting to Centrifugo:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",metastring:'title="Using centrifuge-js v3"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {\n token: token\n});\ncentrifuge.connect();\n'})}),"\n",(0,t.jsxs)(n.p,{children:["See more details about working with connection tokens and handling token expiration on the client-side in the ",(0,t.jsx)(n.a,{href:"/docs/4/transports/client_api#client-connection-token",children:"real-time SDK API spec"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"token-with-expiration",children:"Token with expiration"}),"\n",(0,t.jsx)(n.p,{children:"HS256 token that will be valid for 5 minutes:"}),"\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"token-with-additional-connection-info",children:"Token with additional connection info"}),"\n",(0,t.jsx)(n.p,{children:"Let's attach user name:"}),"\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"investigating-problems-with-jwt",children:"Investigating problems with JWT"}),"\n",(0,t.jsxs)(n.p,{children:["You can use ",(0,t.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,t.jsx)(n.h2,{id:"json-web-key-support",children:"JSON Web Key support"}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo supports JSON Web Key (JWK) ",(0,t.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,t.jsx)(n.code,{children:"kid"})," header of JWT)."]}),"\n",(0,t.jsxs)(n.p,{children:["A mechanism can be enabled by providing ",(0,t.jsx)(n.code,{children:"token_jwks_public_endpoint"})," string option to Centrifugo (HTTP address)."]}),"\n",(0,t.jsxs)(n.p,{children:["As soon as ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["Read a tutorial in our blog about ",(0,t.jsx)(n.a,{href:"/blog/2023/03/31/keycloak-sso-centrifugo",children:"using Centrifugo with Keycloak SSO"}),". In that case connection tokens are verified using public key loaded from the JWKS endpoint of Keycloak."]})}),"\n",(0,t.jsx)(n.p,{children:"At the moment Centrifugo caches keys loaded from an endpoint for one hour."}),"\n",(0,t.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,t.jsxs)(n.p,{children:["Only ",(0,t.jsx)(n.code,{children:"RSA"})," algorithm is supported."]}),"\n",(0,t.jsx)(n.p,{children:"Once enabled JWKS used for both connection and channel subscription tokens."}),"\n",(0,t.jsx)(n.h2,{id:"dynamic-jwks-endpoint",children:"Dynamic JWKs endpoint"}),"\n",(0,t.jsx)(n.p,{children:"Available since Centrifugo v4.1.3"}),"\n",(0,t.jsxs)(n.p,{children:["It's possible to extract variables from ",(0,t.jsx)(n.code,{children:"iss"})," and ",(0,t.jsx)(n.code,{children:"aud"})," JWT claims using ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"Go regexp"})," named groups, then use variables extracted during ",(0,t.jsx)(n.code,{children:"iss"})," or ",(0,t.jsx)(n.code,{children:"aud"})," matching to construct a JWKS endpoint dynamically upon token validation. In this case JWKS endpoint may be set in config as template."]}),"\n",(0,t.jsx)(n.p,{children:"To achieve this Centrifugo provides two additional options:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"token_issuer_regex"})," - match JWT issuer (",(0,t.jsx)(n.code,{children:"iss"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"token_audience_regex"})," - match JWT audience (",(0,t.jsx)(n.code,{children:"aud"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "token_issuer_regex": "https://example.com/auth/realms/(?P [A-z]+)",\n "token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["To use variable in ",(0,t.jsx)(n.code,{children:"token_jwks_public_endpoint"})," it must be wrapped in ",(0,t.jsx)(n.code,{children:"{{"})," ",(0,t.jsx)(n.code,{children:"}}"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["When using ",(0,t.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,t.jsx)(n.code,{children:"token_audience_regex"})," make sure ",(0,t.jsx)(n.code,{children:"token_issuer"})," and ",(0,t.jsx)(n.code,{children:"token_audience"})," not used in the config - otherwise and error will be returned on Centrifugo start."]}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,t.jsx)(n.code,{children:"token_audience_regex"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},32691:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/diagram_jwt_authentication-6a769cc8f218228df5954d240b2057cc.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/7672fb2a.b685cccf.js b/assets/js/7672fb2a.b685cccf.js deleted file mode 100644 index 45fa555ed..000000000 --- a/assets/js/7672fb2a.b685cccf.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8589],{99512:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var t=i(85893),s=i(11151),o=i(74866),r=i(85162);const a={id:"authentication",title:"Client JWT authentication"},c=void 0,l={id:"server/authentication",title:"Client JWT 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-4/server/authentication.md",sourceDirName:"server",slug:"/server/authentication",permalink:"/docs/4/server/authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/server/authentication.md",tags:[],version:"4",frontMatter:{id:"authentication",title:"Client JWT authentication"},sidebar:"Guides",previous:{title:"Server API walkthrough",permalink:"/docs/4/server/server_api"},next:{title:"Channels and namespaces",permalink:"/docs/4/server/channels"}},d={},h=[{value:"Connection JWT claims",id:"connection-jwt-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},{value:"Dynamic JWKs endpoint",id:"dynamic-jwks-endpoint",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,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["To authenticate incoming connection (client) Centrifugo can use ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["If you prefer to avoid using JWT then look at ",(0,t.jsx)(n.a,{href:"/docs/4/server/proxy",children:"the proxy feature"}),". It allows proxying connection requests from Centrifugo to your application backend endpoint for authentication details."]})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["Using JWT auth can be nice in terms of massive reconnect scenario. Since authentication information is encoded directly in the token this may help to drastically reduce load on your application session backend. See in our ",(0,t.jsx)(n.a,{href:"/blog/2020/11/12/scaling-websocket#massive-reconnect",children:"blog post"}),"."]})}),"\n",(0,t.jsx)(n.p,{children:"Upon connecting to Centrifugo client should provide a connection JWT with several predefined credential claims. Here is a diagram:"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(32691).Z+"",width:"2600",height:"906"})}),"\n",(0,t.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,t.jsxs)(n.p,{children:["We will use Javascript Centrifugo client here for example snippets for client-side and ",(0,t.jsx)(n.a,{href:"https://github.com/jpadilla/pyjwt",children:"PyJWT"})," Python library to generate a connection token on the backend side."]}),"\n",(0,t.jsxs)(n.p,{children:["To add HMAC secret key to Centrifugo add ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," to configuration file:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n ...\n "token_hmac_secret_key": " "\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["To add RSA public key (must be PEM encoded string) add ",(0,t.jsx)(n.code,{children:"token_rsa_public_key"})," option, ex:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["To add ECDSA public key (must be PEM encoded string) add ",(0,t.jsx)(n.code,{children:"token_ecdsa_public_key"})," option, ex:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h2,{id:"connection-jwt-claims",children:"Connection JWT claims"}),"\n",(0,t.jsxs)(n.p,{children:["For connection JWT Centrifugo uses the some standart claims defined in ",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"rfc7519"}),", also some custom Centrifugo-specific."]}),"\n",(0,t.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,t.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,t.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,t.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,t.jsx)(n.code,{children:"sub"})," claim. This is called anonymous access. In this case, you may need to enable corresponding channel namespace options which enable access to protocol features for anonymous users."]}),"\n",(0,t.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,t.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,t.jsxs)(n.p,{children:["If ",(0,t.jsx)(n.code,{children:"exp"})," claim is not provided then Centrifugo won't expire connection. When provided special algorithm will find connections with ",(0,t.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,t.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,t.jsxs)(n.p,{children:["Choose ",(0,t.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,t.jsxs)(n.p,{children:["Read more about connection expiration ",(0,t.jsx)(n.a,{href:"#connection-expiration",children:"below"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,t.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,t.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,t.jsx)(n.a,{href:"/docs/4/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,t.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,t.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,t.jsx)(n.a,{href:"/docs/4/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim)."]}),"\n",(0,t.jsxs)(n.p,{children:["But you can force this check by setting ",(0,t.jsx)(n.code,{children:"token_audience"})," string option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_audience": "centrifugo"\n}\n'})}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_audience"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})}),"\n",(0,t.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim)."]}),"\n",(0,t.jsxs)(n.p,{children:["But you can force this check by setting ",(0,t.jsx)(n.code,{children:"token_issuer"})," string option:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_issuer": "my_app"\n}\n'})}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_issuer"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})}),"\n",(0,t.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,t.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,t.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,t.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,t.jsxs)(n.p,{children:["This field contains a ",(0,t.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,t.jsx)(n.h3,{id:"channels",children:"channels"}),"\n",(0,t.jsxs)(n.p,{children:["An optional array of strings with server-side channels to subscribe a client to. See more details about ",(0,t.jsx)(n.a,{href:"/docs/4/server/server_subs",children:"server-side subscriptions"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"subs",children:"subs"}),"\n",(0,t.jsxs)(n.p,{children:["An optional map of channels with options. This is like a ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["This claim is called ",(0,t.jsx)(n.code,{children:"subs"})," as a shortcut from subscriptions. The claim ",(0,t.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,t.jsx)(n.p,{children:"Example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h4,{id:"subscribe-options",children:"Subscribe options:"}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"info"}),(0,t.jsx)(n.td,{children:"JSON object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom channel info"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"b64info"}),(0,t.jsx)(n.td,{children:"string"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom channel info in Base64 - to pass binary channel info"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"data"}),(0,t.jsx)(n.td,{children:"JSON object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Custom JSON data to return in subscription context inside Connect reply"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"b64data"}),(0,t.jsx)(n.td,{children:"string"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["Same as ",(0,t.jsx)(n.code,{children:"data"})," but in Base64 to send binary data"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"override"}),(0,t.jsx)(n.td,{children:"Override object"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.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,t.jsx)(n.h4,{id:"override-object",children:"Override object"}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"presence"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override presence"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override join_leave"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"position"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override position"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"recover"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsx)(n.td,{children:"Override recover"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"BoolValue is an object like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"meta",children:"meta"}),"\n",(0,t.jsxs)(n.p,{children:["Meta is an additional JSON object (ex. ",(0,t.jsx)(n.code,{children:'{"key": "value"}'}),") that will be attached to a connection. Unlike ",(0,t.jsx)(n.code,{children:"info"})," it's never exposed to clients inside presence and join/leave payloads and only accessible on a backend side. It may be included in proxy calls from Centrifugo to the application backend (see ",(0,t.jsx)(n.code,{children:"proxy_include_connection_meta"})," option). Also, there is a ",(0,t.jsx)(n.code,{children:"connections"})," API method in Centrifugo PRO that returns this data in the connection description object."]}),"\n",(0,t.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo looks on ",(0,t.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,t.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,t.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the connection should expire."]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Set it to the future time for expiring connection at some point"}),"\n",(0,t.jsxs)(n.li,{children:["Set it to ",(0,t.jsx)(n.code,{children:"0"})," to disable connection expiration (but still check token ",(0,t.jsx)(n.code,{children:"exp"})," claim)."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"connection-expiration",children:"Connection expiration"}),"\n",(0,t.jsxs)(n.p,{children:["As said above ",(0,t.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,t.jsx)(n.p,{children:"First, you should do is enable client expiration mechanism in Centrifugo providing a connection JWT with expiration:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["Let's suppose that you set ",(0,t.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,t.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["When a client first connects to Centrifugo it receives the ",(0,t.jsx)(n.code,{children:"ttl"})," value in connect reply. That ",(0,t.jsx)(n.code,{children:"ttl"})," value contains the number of seconds after which the client must send the ",(0,t.jsx)(n.code,{children:"refresh"})," command with new credentials to Centrifugo. Centrifugo clients must handle this ",(0,t.jsx)(n.code,{children:"ttl"})," field and automatically start the refresh process."]}),"\n",(0,t.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,t.jsx)(n.code,{children:"/centrifuge/refresh"})," URL endpoint. In response your server must return JSON with a new connection JWT:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'{\n "token": token\n}\n'})}),"\n",(0,t.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,t.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,t.jsx)(n.code,{children:"exp"}),"."]}),"\n",(0,t.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,t.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,t.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,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.p,{children:"Let's look at how to generate connection HS256 JWT in Python:"}),"\n",(0,t.jsx)(n.h3,{id:"simplest-token",children:"Simplest token"}),"\n","\n","\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'import jwt\n\ntoken = jwt.encode({"sub": "42"}, "secret").decode()\n\nprint(token)\n'})})}),(0,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["Note that we use the value of ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo config here (in this case ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," value is just ",(0,t.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,t.jsx)(n.p,{children:"Then you can pass this token to your client side and use it when connecting to Centrifugo:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",metastring:'title="Using centrifuge-js v3"',children:'var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {\n token: token\n});\ncentrifuge.connect();\n'})}),"\n",(0,t.jsxs)(n.p,{children:["See more details about working with connection tokens and handling token expiration on the client-side in the ",(0,t.jsx)(n.a,{href:"/docs/4/transports/client_api#client-connection-token",children:"real-time SDK API spec"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"token-with-expiration",children:"Token with expiration"}),"\n",(0,t.jsx)(n.p,{children:"HS256 token that will be valid for 5 minutes:"}),"\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"token-with-additional-connection-info",children:"Token with additional connection info"}),"\n",(0,t.jsx)(n.p,{children:"Let's attach user name:"}),"\n",(0,t.jsxs)(o.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(r.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(r.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"investigating-problems-with-jwt",children:"Investigating problems with JWT"}),"\n",(0,t.jsxs)(n.p,{children:["You can use ",(0,t.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,t.jsx)(n.h2,{id:"json-web-key-support",children:"JSON Web Key support"}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo supports JSON Web Key (JWK) ",(0,t.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,t.jsx)(n.code,{children:"kid"})," header of JWT)."]}),"\n",(0,t.jsxs)(n.p,{children:["A mechanism can be enabled by providing ",(0,t.jsx)(n.code,{children:"token_jwks_public_endpoint"})," string option to Centrifugo (HTTP address)."]}),"\n",(0,t.jsxs)(n.p,{children:["As soon as ",(0,t.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,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsxs)(n.p,{children:["Read a tutorial in our blog about ",(0,t.jsx)(n.a,{href:"/blog/2023/03/31/keycloak-sso-centrifugo",children:"using Centrifugo with Keycloak SSO"}),". In that case connection tokens are verified using public key loaded from the JWKS endpoint of Keycloak."]})}),"\n",(0,t.jsx)(n.p,{children:"At the moment Centrifugo caches keys loaded from an endpoint for one hour."}),"\n",(0,t.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,t.jsxs)(n.p,{children:["Only ",(0,t.jsx)(n.code,{children:"RSA"})," algorithm is supported."]}),"\n",(0,t.jsx)(n.p,{children:"Once enabled JWKS used for both connection and channel subscription tokens."}),"\n",(0,t.jsx)(n.h2,{id:"dynamic-jwks-endpoint",children:"Dynamic JWKs endpoint"}),"\n",(0,t.jsx)(n.p,{children:"Available since Centrifugo v4.1.3"}),"\n",(0,t.jsxs)(n.p,{children:["It's possible to extract variables from ",(0,t.jsx)(n.code,{children:"iss"})," and ",(0,t.jsx)(n.code,{children:"aud"})," JWT claims using ",(0,t.jsx)(n.a,{href:"https://pkg.go.dev/regexp",children:"Go regexp"})," named groups, then use variables extracted during ",(0,t.jsx)(n.code,{children:"iss"})," or ",(0,t.jsx)(n.code,{children:"aud"})," matching to construct a JWKS endpoint dynamically upon token validation. In this case JWKS endpoint may be set in config as template."]}),"\n",(0,t.jsx)(n.p,{children:"To achieve this Centrifugo provides two additional options:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"token_issuer_regex"})," - match JWT issuer (",(0,t.jsx)(n.code,{children:"iss"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"token_audience_regex"})," - match JWT audience (",(0,t.jsx)(n.code,{children:"aud"})," claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "token_issuer_regex": "https://example.com/auth/realms/(?P [A-z]+)",\n "token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["To use variable in ",(0,t.jsx)(n.code,{children:"token_jwks_public_endpoint"})," it must be wrapped in ",(0,t.jsx)(n.code,{children:"{{"})," ",(0,t.jsx)(n.code,{children:"}}"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["When using ",(0,t.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,t.jsx)(n.code,{children:"token_audience_regex"})," make sure ",(0,t.jsx)(n.code,{children:"token_issuer"})," and ",(0,t.jsx)(n.code,{children:"token_audience"})," not used in the config - otherwise and error will be returned on Centrifugo start."]}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["Setting ",(0,t.jsx)(n.code,{children:"token_issuer_regex"})," and ",(0,t.jsx)(n.code,{children:"token_audience_regex"})," will also affect subscription tokens (used for ",(0,t.jsx)(n.a,{href:"/docs/4/server/channel_token_auth",children:"channel token authorization"}),"). Please read ",(0,t.jsx)(n.a,{href:"https://github.com/centrifugal/centrifugo/issues/640",children:"this issue"})," and reach out if your use case requires separate configuration for subscription tokens."]})})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},85162:(e,n,i)=>{i.d(n,{Z:()=>r});i(67294);var t=i(36905);const s={tabItem:"tabItem_Ymn6"};var o=i(85893);function r(e){let{children:n,hidden:i,className:r}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,t.Z)(s.tabItem,r),hidden:i,children:n})}},74866:(e,n,i)=>{i.d(n,{Z:()=>k});var t=i(67294),s=i(36905),o=i(12466),r=i(16550),a=i(20469),c=i(91980),l=i(67392),d=i(50012);function h(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.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 u(e){const{values:n,children:i}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:i,attributes:t,default:s}}=e;return{value:n,label:i,attributes:t,default:s}}))}(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 x(e){let{queryString:n=!1,groupId:i}=e;const s=(0,r.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,t.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:i=!1,groupId:s}=e,o=u(e),[r,c]=(0,t.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 t=i.find((e=>e.default))??i[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:o}))),[l,h]=x({queryString:i,groupId:s}),[j,f]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,o]=(0,d.Nk)(i);return[s,(0,t.useCallback)((e=>{i&&o.set(e)}),[i,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,t.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=i(72389);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var b=i(85893);function g(e){let{className:n,block:i,selectedValue:t,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.o5)(),d=e=>{const n=e.currentTarget,i=c.indexOf(n),s=a[i].value;s!==t&&(l(n),r(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(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,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":i},n),children:a.map((e=>{let{value:n,label:i,attributes:o}=e;return(0,b.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...o,className:(0,s.Z)("tabs__item",m.tabItem,o?.className,{"tabs__item--active":t===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:s}=e;const o=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,t.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,{...e,...n}),(0,b.jsx)(v,{...e,...n})]})}function k(e){const n=(0,f.Z)();return(0,b.jsx)(y,{...e,children:h(e.children)},String(n))}},32691:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/diagram_jwt_authentication-6a769cc8f218228df5954d240b2057cc.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/7747d83f.2d326f7e.js b/assets/js/7747d83f.2d326f7e.js new file mode 100644 index 000000000..b669dbb8f --- /dev/null +++ b/assets/js/7747d83f.2d326f7e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9878],{95114:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>h,frontMatter:()=>c,metadata:()=>t,toc:()=>l});var i=s(85893),r=s(11151);const c={id:"cel_expressions",sidebar_label:"Channel CEL expressions",title:"Channel CEL expressions"},o=void 0,t={id:"pro/cel_expressions",title:"Channel CEL expressions",description:"Centrifugo PRO supports CEL expressions (Common Expression Language) for checking channel operation permissions.",source:"@site/docs/pro/cel_expressions.md",sourceDirName:"pro",slug:"/pro/cel_expressions",permalink:"/docs/pro/cel_expressions",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/cel_expressions.md",tags:[],version:"current",frontMatter:{id:"cel_expressions",sidebar_label:"Channel CEL expressions",title:"Channel CEL expressions"},sidebar:"Pro",previous:{title:"Channel patterns",permalink:"/docs/pro/channel_patterns"},next:{title:"Faster performance",permalink:"/docs/pro/performance"}},a={},l=[{value:"subscribe_cel",id:"subscribe_cel",level:2},{value:"Expression variables",id:"expression-variables",level:3},{value:"publish_cel",id:"publish_cel",level:2},{value:"history_cel",id:"history_cel",level:2},{value:"presence_cel",id:"presence_cel",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["Centrifugo PRO supports ",(0,i.jsx)(n.a,{href:"https://opensource.google/projects/cel",children:"CEL expressions"})," (Common Expression Language) for checking channel operation permissions."]}),"\n",(0,i.jsx)(n.p,{children:"CEL expressions provide a developer-friendly, fast and secure way to evaluate some conditions predefined in the configuration. They are used in some Google services (ex. Firebase), in Envoy RBAC configuration, etc."}),"\n",(0,i.jsx)(n.p,{children:"For Centrifugo this is a flexible mechanism which can help to avoid using subscription tokens or using subscribe proxy in some cases. This means you can avoid sending an additional HTTP request to the backend for a channel subscription attempt. As the result less resources may be used and smaller latencies may be achieved in the system. This is a way to introduce efficient channel permission mechanics when Centrifugo built-in rules are not enough."}),"\n",(0,i.jsx)(n.p,{children:"Some good links which may help you dive into CEL expressions are:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/google/cel-spec/blob/master/doc/intro.md",children:"CEL introduction"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/google/cel-spec/blob/master/doc/langdef.md",children:"CEL language definition"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://cloud.google.com/asset-inventory/docs/monitoring-asset-changes-with-condition#using_cel",children:"Docs of Google asset inventory"})," which also uses CEL"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Below we will explore some basic expressions and show how they can be used in Centrifugo."}),"\n",(0,i.jsx)(n.h2,{id:"subscribe_cel",children:"subscribe_cel"}),"\n",(0,i.jsx)(n.p,{children:"We suppose that the main operation for which developers may use CEL expressions in Centrifugo is a subscribe operation. Let's look at it in detail."}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to configure ",(0,i.jsx)(n.code,{children:"subscribe_cel"})," for a channel namespace (",(0,i.jsx)(n.code,{children:"subscribe_cel"})," is just an additional namespace ",(0,i.jsx)(n.a,{href:"/docs/server/channels#channel-options",children:"channel option"}),", with same rules applied). This expression should be a valid CEL expression."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "namespaces": [\n {\n "name": "admin",\n "subscribe_cel": "\'admin\' in meta.roles"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["In the example we are using custom ",(0,i.jsx)(n.code,{children:"meta"})," information (must be an object) attached to the connection. As mentioned before in the doc this meta may be attached to the connection:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["when set in the ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#connect-proxy",children:"connect proxy"})," result"]}),"\n",(0,i.jsxs)(n.li,{children:["or provided in JWT as ",(0,i.jsx)(n.a,{href:"/docs/server/authentication#meta",children:"meta"})," claim"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["An expression is evaluated for every subscription attempt to a channel in a namespace. So if ",(0,i.jsx)(n.code,{children:"meta"})," attached to the connection is sth like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "roles": ["admin"]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["\u2013 then for every channel in the ",(0,i.jsx)(n.code,{children:"admin"})," namespace defined above expression will be evaluated to ",(0,i.jsx)(n.code,{children:"True"})," and subscription will be accepted by Centrifugo."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"meta"})," must be JSON object (any ",(0,i.jsx)(n.code,{children:"{}"}),") for CEL expressions to work."]})}),"\n",(0,i.jsx)(n.h3,{id:"expression-variables",children:"Expression variables"}),"\n",(0,i.jsx)(n.p,{children:"Inside the expression developers can use some variables which are injected by Centrifugo to the CEL runtime."}),"\n",(0,i.jsxs)(n.p,{children:["Information about current ",(0,i.jsx)(n.code,{children:"user"})," ID, ",(0,i.jsx)(n.code,{children:"meta"})," information attached to the connection, all the variables defined in matched ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," will be available for CEL expression evaluation."]}),"\n",(0,i.jsxs)(n.p,{children:["Say client with user ID ",(0,i.jsx)(n.code,{children:"123"})," subscribes to a channel ",(0,i.jsx)(n.code,{children:"/users/4"})," which matched the ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," ",(0,i.jsx)(n.code,{children:"/users/:user"}),":"]}),"\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:"Variable"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Example"}),(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:"subscribed"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"bool"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"false"})}),(0,i.jsxs)(n.td,{children:["Whether client is subscribed to channel, always ",(0,i.jsx)(n.code,{children:"false"})," for ",(0,i.jsx)(n.code,{children:"subscribe"})," operation"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"user"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"123"'})}),(0,i.jsx)(n.td,{children:"Current authenticated user ID (known from from JWT or connect proxy result)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"meta"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"map[string]any"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'{"roles": ["admin"]}'})}),(0,i.jsx)(n.td,{children:"Meta information attached to the connection by the apllication backend (in JWT or over connect proxy result)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"channel"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"/users/4"'})}),(0,i.jsx)(n.td,{children:"Channel client tries to subscribe"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"vars"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"map[string]string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'{"user": "4"}'})}),(0,i.jsx)(n.td,{children:"Extracted variables from the matched channel pattern. It's empty in case of using channels without variables."})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"In this case, to allow admin to subscribe on any user's channel or allow non-admin user to subscribe only on its own channel, you may construct an expression like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "subscribe_cel": "vars.user == user or \'admin\' in meta.roles"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Let's look at one more example. Say client with user ID ",(0,i.jsx)(n.code,{children:"123"})," subscribes to a channel ",(0,i.jsx)(n.code,{children:"/example.com/users/4"})," which matched the ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," ",(0,i.jsx)(n.code,{children:"/:tenant/users/:user"}),". The permission check may be transformed into sth like this (assuming ",(0,i.jsx)(n.code,{children:"meta"})," information has information about current connection tenant):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "/:tenant/users/:user",\n "subscribe_cel": "vars.tenant == meta.tenant && (vars.user == user or \'admin\' in meta.roles)"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publish_cel",children:"publish_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions to publish into a channel. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]}),"\n",(0,i.jsx)(n.h2,{id:"history_cel",children:"history_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions for channel history. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]}),"\n",(0,i.jsx)(n.h2,{id:"presence_cel",children:"presence_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions for channel presence. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]})]})}function h(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:()=>t,a:()=>o});var i=s(67294);const r={},c=i.createContext(r);function o(e){const n=i.useContext(c);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function t(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7747d83f.f4e0ee01.js b/assets/js/7747d83f.f4e0ee01.js deleted file mode 100644 index 3fd584701..000000000 --- a/assets/js/7747d83f.f4e0ee01.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9878],{95114:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>h,frontMatter:()=>c,metadata:()=>t,toc:()=>l});var i=s(85893),r=s(11151);const c={id:"cel_expressions",sidebar_label:"CEL expressions",title:"CEL expressions"},o=void 0,t={id:"pro/cel_expressions",title:"CEL expressions",description:"Centrifugo PRO supports CEL expressions (Common Expression Language) for checking channel operation permissions.",source:"@site/docs/pro/cel_expressions.md",sourceDirName:"pro",slug:"/pro/cel_expressions",permalink:"/docs/pro/cel_expressions",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/cel_expressions.md",tags:[],version:"current",frontMatter:{id:"cel_expressions",sidebar_label:"CEL expressions",title:"CEL expressions"},sidebar:"Pro",previous:{title:"Channel patterns",permalink:"/docs/pro/channel_patterns"},next:{title:"Faster performance",permalink:"/docs/pro/performance"}},a={},l=[{value:"subscribe_cel",id:"subscribe_cel",level:2},{value:"Expression variables",id:"expression-variables",level:3},{value:"publish_cel",id:"publish_cel",level:2},{value:"history_cel",id:"history_cel",level:2},{value:"presence_cel",id:"presence_cel",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["Centrifugo PRO supports ",(0,i.jsx)(n.a,{href:"https://opensource.google/projects/cel",children:"CEL expressions"})," (Common Expression Language) for checking channel operation permissions."]}),"\n",(0,i.jsx)(n.p,{children:"CEL expressions provide a developer-friendly, fast and secure way to evaluate some conditions predefined in the configuration. They are used in some Google services (ex. Firebase), in Envoy RBAC configuration, etc."}),"\n",(0,i.jsx)(n.p,{children:"For Centrifugo this is a flexible mechanism which can help to avoid using subscription tokens or using subscribe proxy in some cases. This means you can avoid sending an additional HTTP request to the backend for a channel subscription attempt. As the result less resources may be used and smaller latencies may be achieved in the system. This is a way to introduce efficient channel permission mechanics when Centrifugo built-in rules are not enough."}),"\n",(0,i.jsx)(n.p,{children:"Some good links which may help you dive into CEL expressions are:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/google/cel-spec/blob/master/doc/intro.md",children:"CEL introduction"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/google/cel-spec/blob/master/doc/langdef.md",children:"CEL language definition"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://cloud.google.com/asset-inventory/docs/monitoring-asset-changes-with-condition#using_cel",children:"Docs of Google asset inventory"})," which also uses CEL"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Below we will explore some basic expressions and show how they can be used in Centrifugo."}),"\n",(0,i.jsx)(n.h2,{id:"subscribe_cel",children:"subscribe_cel"}),"\n",(0,i.jsx)(n.p,{children:"We suppose that the main operation for which developers may use CEL expressions in Centrifugo is a subscribe operation. Let's look at it in detail."}),"\n",(0,i.jsxs)(n.p,{children:["It's possible to configure ",(0,i.jsx)(n.code,{children:"subscribe_cel"})," for a channel namespace (",(0,i.jsx)(n.code,{children:"subscribe_cel"})," is just an additional namespace ",(0,i.jsx)(n.a,{href:"/docs/server/channels#channel-options",children:"channel option"}),", with same rules applied). This expression should be a valid CEL expression."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "namespaces": [\n {\n "name": "admin",\n "subscribe_cel": "\'admin\' in meta.roles"\n }\n ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["In the example we are using custom ",(0,i.jsx)(n.code,{children:"meta"})," information (must be an object) attached to the connection. As mentioned before in the doc this meta may be attached to the connection:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["when set in the ",(0,i.jsx)(n.a,{href:"/docs/server/proxy#connect-proxy",children:"connect proxy"})," result"]}),"\n",(0,i.jsxs)(n.li,{children:["or provided in JWT as ",(0,i.jsx)(n.a,{href:"/docs/server/authentication#meta",children:"meta"})," claim"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["An expression is evaluated for every subscription attempt to a channel in a namespace. So if ",(0,i.jsx)(n.code,{children:"meta"})," attached to the connection is sth like this:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "roles": ["admin"]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["\u2013 then for every channel in the ",(0,i.jsx)(n.code,{children:"admin"})," namespace defined above expression will be evaluated to ",(0,i.jsx)(n.code,{children:"True"})," and subscription will be accepted by Centrifugo."]}),"\n",(0,i.jsx)(n.admonition,{type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"meta"})," must be JSON object (any ",(0,i.jsx)(n.code,{children:"{}"}),") for CEL expressions to work."]})}),"\n",(0,i.jsx)(n.h3,{id:"expression-variables",children:"Expression variables"}),"\n",(0,i.jsx)(n.p,{children:"Inside the expression developers can use some variables which are injected by Centrifugo to the CEL runtime."}),"\n",(0,i.jsxs)(n.p,{children:["Information about current ",(0,i.jsx)(n.code,{children:"user"})," ID, ",(0,i.jsx)(n.code,{children:"meta"})," information attached to the connection, all the variables defined in matched ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," will be available for CEL expression evaluation."]}),"\n",(0,i.jsxs)(n.p,{children:["Say client with user ID ",(0,i.jsx)(n.code,{children:"123"})," subscribes to a channel ",(0,i.jsx)(n.code,{children:"/users/4"})," which matched the ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," ",(0,i.jsx)(n.code,{children:"/users/:user"}),":"]}),"\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:"Variable"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Example"}),(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:"subscribed"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"bool"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"false"})}),(0,i.jsxs)(n.td,{children:["Whether client is subscribed to channel, always ",(0,i.jsx)(n.code,{children:"false"})," for ",(0,i.jsx)(n.code,{children:"subscribe"})," operation"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"user"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"123"'})}),(0,i.jsx)(n.td,{children:"Current authenticated user ID (known from from JWT or connect proxy result)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"meta"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"map[string]any"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'{"roles": ["admin"]}'})}),(0,i.jsx)(n.td,{children:"Meta information attached to the connection by the apllication backend (in JWT or over connect proxy result)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"channel"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'"/users/4"'})}),(0,i.jsx)(n.td,{children:"Channel client tries to subscribe"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"vars"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"map[string]string"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'{"user": "4"}'})}),(0,i.jsx)(n.td,{children:"Extracted variables from the matched channel pattern. It's empty in case of using channels without variables."})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"In this case, to allow admin to subscribe on any user's channel or allow non-admin user to subscribe only on its own channel, you may construct an expression like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n ...\n "subscribe_cel": "vars.user == user or \'admin\' in meta.roles"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Let's look at one more example. Say client with user ID ",(0,i.jsx)(n.code,{children:"123"})," subscribes to a channel ",(0,i.jsx)(n.code,{children:"/example.com/users/4"})," which matched the ",(0,i.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"channel pattern"})," ",(0,i.jsx)(n.code,{children:"/:tenant/users/:user"}),". The permission check may be transformed into sth like this (assuming ",(0,i.jsx)(n.code,{children:"meta"})," information has information about current connection tenant):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "namespaces": [\n {\n "name": "/:tenant/users/:user",\n "subscribe_cel": "vars.tenant == meta.tenant && (vars.user == user or \'admin\' in meta.roles)"\n }\n ]\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publish_cel",children:"publish_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions to publish into a channel. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]}),"\n",(0,i.jsx)(n.h2,{id:"history_cel",children:"history_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions for channel history. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]}),"\n",(0,i.jsx)(n.h2,{id:"presence_cel",children:"presence_cel"}),"\n",(0,i.jsxs)(n.p,{children:["CEL expression to check permissions for channel presence. ",(0,i.jsx)(n.a,{href:"#expression-variables",children:"Same expression variables"})," are available."]})]})}function h(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:()=>t,a:()=>o});var i=s(67294);const r={},c=i.createContext(r);function o(e){const n=i.useContext(c);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function t(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7908.9fe1a6e9.js b/assets/js/7908.9fe1a6e9.js deleted file mode 100644 index 47a74a226..000000000 --- a/assets/js/7908.9fe1a6e9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7908],{59047:(e,n,t)=>{t.d(n,{Z:()=>w});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),o=t(95999),r=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)(r.k.common.admonition,r.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:[(0,s.jsx)(u,{title:i,icon:t}),(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)(o.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 g(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)(g,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function p(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 C={icon:(0,s.jsx)(N,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.info",description:"The default label used for the Info admonition (:::info)",children:"info"})};function L(e){return(0,s.jsx)(h,{...C,...e,className:(0,l.Z)("alert alert--info",e.className),children:e.children})}function b(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 Z={icon:(0,s.jsx)(b,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function H(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 y={icon:(0,s.jsx)(H,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const k={icon:(0,s.jsx)(b,{}),title:(0,s.jsx)(o.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const _={...{note:v,tip:p,info:L,warning:function(e){return(0,s.jsx)(h,{...Z,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,s.jsx)(h,{...y,...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)(L,{title:"important",...e}),success:e=>(0,s.jsx)(p,{title:"success",...e}),caution:function(e){return(0,s.jsx)(h,{...k,...e,className:(0,l.Z)("alert alert--warning",e.className),children:e.children})}}};function w(e){const n=a(e),t=(i=n.type,_[i]||(console.warn(`No admonition component found for admonition type "${i}". Using Info as fallback.`),_.info));var i;return(0,s.jsx)(t,{...n})}},40591:(e,n,t)=>{t.d(n,{Z:()=>_});var i=t(67294),s=t(11151),a=t(35742),l=t(9286),o=t(85893);var r=t(39960);var c=t(36905),d=t(788),u=t(72389),m=t(86043);const h={details:"details_lb9f",isBrowser:"isBrowser_bmU9",collapsibleContent:"collapsibleContent_i85q"};function f(e){return!!e&&("SUMMARY"===e.tagName||f(e.parentElement))}function x(e,n){return!!e&&(e===n||x(e.parentElement,n))}function v(e){let{summary:n,children:t,...s}=e;const a=(0,u.Z)(),l=(0,i.useRef)(null),{collapsed:r,setCollapsed:c}=(0,m.u)({initialState:!s.open}),[v,g]=(0,i.useState)(s.open),j=i.isValidElement(n)?n:(0,o.jsx)("summary",{children:n??"Details"});return(0,o.jsxs)("details",{...s,ref:l,open:v,"data-collapsed":r,className:(0,d.Z)(h.details,a&&h.isBrowser,s.className),onMouseDown:e=>{f(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();const n=e.target;f(n)&&x(n,l.current)&&(e.preventDefault(),r?(c(!1),g(!0)):c(!0))},children:[j,(0,o.jsx)(m.z,{lazy:!1,collapsed:r,disableSSRStyle:!0,onCollapseTransitionEnd:e=>{c(e),g(!e)},children:(0,o.jsx)("div",{className:h.collapsibleContent,children:t})})]})}const g={details:"details_b_Ee"},j="alert alert--info";function p(e){let{...n}=e;return(0,o.jsx)(v,{...n,className:(0,c.Z)(j,g.details,n.className)})}function N(e){const n=i.Children.toArray(e.children),t=n.find((e=>i.isValidElement(e)&&"summary"===e.type)),s=(0,o.jsx)(o.Fragment,{children:n.filter((e=>e!==t))});return(0,o.jsx)(p,{...e,summary:t,children:s})}var C=t(92503);function L(e){return(0,o.jsx)(C.Z,{...e})}const b={containsTaskList:"containsTaskList_mC6p"};function Z(e){if(void 0!==e)return(0,c.Z)(e,e?.includes("contains-task-list")&&b.containsTaskList)}const H={img:"img_ev3q"};var y=t(59047);const k={Head:a.Z,details:N,Details:N,code:function(e){return i.Children.toArray(e.children).every((e=>"string"==typeof e&&!e.includes("\n")))?(0,o.jsx)("code",{...e}):(0,o.jsx)(l.Z,{...e})},a:function(e){return(0,o.jsx)(r.Z,{...e})},pre:function(e){return(0,o.jsx)(o.Fragment,{children:e.children})},ul:function(e){return(0,o.jsx)("ul",{...e,className:Z(e.className)})},img:function(e){return(0,o.jsx)("img",{loading:"lazy",...e,className:(n=e.className,(0,c.Z)(n,H.img))});var n},h1:e=>(0,o.jsx)(L,{as:"h1",...e}),h2:e=>(0,o.jsx)(L,{as:"h2",...e}),h3:e=>(0,o.jsx)(L,{as:"h3",...e}),h4:e=>(0,o.jsx)(L,{as:"h4",...e}),h5:e=>(0,o.jsx)(L,{as:"h5",...e}),h6:e=>(0,o.jsx)(L,{as:"h6",...e}),admonition:y.Z,mermaid:()=>null};function _(e){let{children:n}=e;return(0,o.jsx)(s.Z,{components:k,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 o="table-of-contents__link toc-highlight",r="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:o,linkActiveClassName:r})})}},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 o(e){const n=e.getBoundingClientRect();return n.top===n.bottom?o(e.parentNode):n}function r(e,n){let{anchorTopOffset:t}=n;const i=e.find((e=>o(e).top>=t));if(i){return function(e){return e.top>0&&e.bottom {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 o(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(i),o=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=r(o,{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",o),document.addEventListener("resize",o),o(),()=>{document.removeEventListener("scroll",o),document.removeEventListener("resize",o)}}),[e,t])}var u=t(39960),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:o="table-of-contents__link",linkActiveClassName:r,minHeadingLevel:c,maxHeadingLevel:u,...h}=e;const x=(0,s.L)(),v=c??x.tableOfContents.minHeadingLevel,g=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:g});return d((0,i.useMemo)((()=>{if(o&&r)return{linkClassName:o,linkActiveClassName:r,minHeadingLevel:v,maxHeadingLevel:g}}),[o,r,v,g])),(0,m.jsx)(f,{toc:j,className:t,linkClassName:o,...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 o(){return(0,l.jsx)(s.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function r(){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)(o,{}),className:(0,i.Z)(n,d.k.common.unlistedBanner),children:(0,l.jsx)(r,{})})}function h(e){return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c,{}),(0,l.jsx)(m,{...e})]})}}}]); \ No newline at end of file diff --git a/assets/js/7bd30152.44eccb8e.js b/assets/js/7bd30152.44eccb8e.js deleted file mode 100644 index a9a9de082..000000000 --- a/assets/js/7bd30152.44eccb8e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2635],{56575:(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/versioned_docs/version-4/getting-started/migration-v4.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v4",permalink:"/docs/4/getting-started/migration_v4",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/getting-started/migration-v4.md",tags:[],version:"4",frontMatter:{id:"migration_v4",title:"Migrating to v4"},sidebar:"Introduction",previous:{title:"Ecosystem notes",permalink:"/docs/4/getting-started/ecosystem"}},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/4/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/4/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","\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/4/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/7bd30152.87620c16.js b/assets/js/7bd30152.87620c16.js new file mode 100644 index 000000000..dfece0cee --- /dev/null +++ b/assets/js/7bd30152.87620c16.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2635],{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})]})}}},56575:(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/versioned_docs/version-4/getting-started/migration-v4.md",sourceDirName:"getting-started",slug:"/getting-started/migration_v4",permalink:"/docs/4/getting-started/migration_v4",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/versioned_docs/version-4/getting-started/migration-v4.md",tags:[],version:"4",frontMatter:{id:"migration_v4",title:"Migrating to v4"},sidebar:"Introduction",previous:{title:"Ecosystem notes",permalink:"/docs/4/getting-started/ecosystem"}},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/4/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/4/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","\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/4/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/84b1c6a7.76cdf790.js b/assets/js/84b1c6a7.76cdf790.js deleted file mode 100644 index 4ae387cc2..000000000 --- a/assets/js/84b1c6a7.76cdf790.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6e3],{62613:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>c,metadata:()=>o,toc:()=>l});var t=s(85893),i=s(11151);const c={id:"client_api",title:"Client API showcase"},r=void 0,o={id:"getting-started/client_api",title:"Client API showcase",description:"This chapter showcases the capabilities of Centrifugo's bidirectional client API \u2013 i.e., the real-time messaging primitives available on the front end (which can be a browser or a mobile device).",source:"@site/docs/getting-started/client_api.md",sourceDirName:"getting-started",slug:"/getting-started/client_api",permalink:"/docs/getting-started/client_api",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/getting-started/client_api.md",tags:[],version:"current",frontMatter:{id:"client_api",title:"Client API showcase"}},a={},l=[{value:"Connecting to a server",id:"connecting-to-a-server",level:2},{value:"Disconnecting from a server",id:"disconnecting-from-a-server",level:2},{value:"Reconnecting to a server",id:"reconnecting-to-a-server",level:2},{value:"Connection lifecycle events",id:"connection-lifecycle-events",level:2},{value:"Subscribe to a channel",id:"subscribe-to-a-channel",level:2},{value:"Server-side subscriptions",id:"server-side-subscriptions",level:2},{value:"Send RPC",id:"send-rpc",level:2},{value:"Call channel history",id:"call-channel-history",level:2},{value:"Presence and presence stats",id:"presence-and-presence-stats",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:"This chapter showcases the capabilities of Centrifugo's bidirectional client API \u2013 i.e., the real-time messaging primitives available on the front end (which can be a browser or a mobile device)."}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"It is also possible to avoid using the client library at all with unidirectional transports (../transports/overview.md)."})}),"\n",(0,t.jsxs)(n.p,{children:["This is a formal description \u2013 we use the JavaScript client ",(0,t.jsx)(n.code,{children:"centrifuge-js"})," for examples here. Refer to each specific client implementation for concrete method names and possibilities. See the ",(0,t.jsx)(n.a,{href:"/docs/transports/client_sdk",children:"full list of Centrifugo client SDKs"}),". This description does not cover all protocol possibilities \u2013 just the most important ones to start with."]}),"\n",(0,t.jsxs)(n.p,{children:["If you are looking for detailed information about the client-server protocol internals, then the ",(0,t.jsx)(n.a,{href:"/docs/transports/client_protocol",children:"client protocol description"})," chapter can help."]}),"\n",(0,t.jsx)(n.h2,{id:"connecting-to-a-server",children:"Connecting to a server"}),"\n",(0,t.jsx)(n.p,{children:"Each Centrifugo client allows connecting to a server."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket');\ncentrifuge.connect();\n"})}),"\n",(0,t.jsx)(n.p,{children:"In most cases you will need to pass JWT (JSON Web Token) for authentication, so the example above transforms to:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket');\ncentrifuge.setToken(' ')\ncentrifuge.connect();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["See ",(0,t.jsx)(n.a,{href:"/docs/server/authentication",children:"authentication"})," chapter for more information on how to generate connection JWT."]}),"\n",(0,t.jsxs)(n.p,{children:["If you are using ",(0,t.jsx)(n.a,{href:"/docs/server/proxy#connect-proxy",children:"connect proxy"})," then you may go without setting JWT."]}),"\n",(0,t.jsx)(n.h2,{id:"disconnecting-from-a-server",children:"Disconnecting from a server"}),"\n",(0,t.jsx)(n.p,{children:"After connecting you can disconnect from a server at any moment."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"centrifuge.disconnect();\n"})}),"\n",(0,t.jsx)(n.h2,{id:"reconnecting-to-a-server",children:"Reconnecting to a server"}),"\n",(0,t.jsx)(n.p,{children:"Centrifugo clients automatically reconnect to a server in case of temporary connection loss, also clients periodically ping the server to detect broken connections."}),"\n",(0,t.jsx)(n.h2,{id:"connection-lifecycle-events",children:"Connection lifecycle events"}),"\n",(0,t.jsx)(n.p,{children:"All client implementations allow setting handlers on connect and disconnect events."}),"\n",(0,t.jsx)(n.p,{children:"For example:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"centrifuge.on('connect', function(connectCtx){\n console.log('connected', connectCtx)\n});\n\ncentrifuge.on('disconnect', function(disconnectCtx){\n console.log('disconnected', disconnectCtx)\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"subscribe-to-a-channel",children:"Subscribe to a channel"}),"\n",(0,t.jsx)(n.p,{children:"Another core functionality of client API is the possibility to subscribe to a channel to receive all messages published to that channel."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"centrifuge.subscribe('channel', function(messageCtx) {\n console.log(messageCtx);\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Clients can subscribe to ",(0,t.jsx)(n.a,{href:"/docs/server/channels",children:"different channels"}),". The subscribe method returns the ",(0,t.jsx)(n.code,{children:"Subscription"})," object. It's also possible to react to different Subscription events: join and leave events, subscribe success and subscribe error events, unsubscribe events."]}),"\n",(0,t.jsxs)(n.p,{children:["In the idiomatic case, messages are published to channels from the application backend ",(0,t.jsx)(n.a,{href:"/docs/server/server_api",children:"over the Centrifugo server API"}),". However, this is not always the case."]}),"\n",(0,t.jsxs)(n.p,{children:["Centrifugo also provides a message recovery feature to restore missed publications in channels. Publications can be missed due to temporary disconnects (bad network) or server reloads. Recovery happens automatically on reconnect (due to bad network or server reloads) as soon as recovery in the channel is ",(0,t.jsx)(n.a,{href:"/docs/server/channels#channel-options",children:"properly configured"}),". The client keeps the last seen Publication offset and restores missed publications since the known offset upon reconnecting. If recovery fails, then the client implementation provides a flag inside the subscribe event to let the application know that some publications were missed \u2013 so you may need to load the state from scratch from the application backend. Not all Centrifugo clients implement a recovery feature \u2013 refer to specific client implementation docs. More details about recovery can be found in ",(0,t.jsx)(n.a,{href:"/docs/server/history_and_recovery",children:"a dedicated chapter"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"server-side-subscriptions",children:"Server-side subscriptions"}),"\n",(0,t.jsxs)(n.p,{children:["To handle publications coming from ",(0,t.jsx)(n.a,{href:"/docs/server/server_subs",children:"server-side subscriptions"})," client API allows listening publications simply on Centrifuge client instance:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"centrifuge.on('publish', function(messageCtx) {\n console.log(messageCtx);\n});\n"})}),"\n",(0,t.jsx)(n.p,{children:"It's also possible to react on different server-side Subscription events: join and leave events, subscribe success, unsubscribe event. There is no subscribe error event here since the subscription was initiated on the server-side."}),"\n",(0,t.jsx)(n.h2,{id:"send-rpc",children:"Send RPC"}),"\n",(0,t.jsxs)(n.p,{children:["A client can send an RPC to the server. RPC is a call that is not related to channels at all. It's just a way to call a server method from the client side over the WebSocket or SockJS connection. RPC is only available when an ",(0,t.jsx)(n.a,{href:"/docs/server/proxy#rpc-proxy",children:"RPC proxy"})," is configured."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const rpcRequest = {'key': 'value'};\nconst data = await centrifuge.namedRPC('example_method', rpcRequest);\n"})}),"\n",(0,t.jsx)(n.h2,{id:"call-channel-history",children:"Call channel history"}),"\n",(0,t.jsxs)(n.p,{children:["Once subscribed client can call publication history inside a channel (only for channels where ",(0,t.jsx)(n.a,{href:"/docs/server/channels#channel-options",children:"history configured"}),") to get last publications in channel:"]}),"\n",(0,t.jsx)(n.p,{children:"Get stream current top position:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history();\nconsole.log(resp.offset);\nconsole.log(resp.epoch);\n"})}),"\n",(0,t.jsx)(n.p,{children:"Get up to 10 publications from history since known stream position:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.p,{children:"Get up to 10 publications from history since current stream beginning:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10});\nconsole.log(resp.publications);\n"})}),"\n",(0,t.jsx)(n.p,{children:"Get up to 10 publications from history since current stream end in reversed order (last to first):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const resp = await subscription.history({limit: 10, reverse: true});\nconsole.log(resp.publications);\n"})}),"\n",(0,t.jsx)(n.h2,{id:"presence-and-presence-stats",children:"Presence and presence stats"}),"\n",(0,t.jsxs)(n.p,{children:["Once subscribed client can call presence and presence stats information inside channel (only for channels where ",(0,t.jsx)(n.a,{href:"/docs/server/channels#channel-options",children:"presence configured"}),"):"]}),"\n",(0,t.jsx)(n.p,{children:"For presence (full information about active subscribers in channel):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.p,{children:"For presence stats (just a number of clients and unique users in a channel):"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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"})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},11151:(e,n,s)=>{s.d(n,{Z:()=>o,a:()=>r});var t=s(67294);const i={},c=t.createContext(i);function r(e){const n=t.useContext(c);return t.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(i):e.components||i:r(e.components),t.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.8ed04548.js b/assets/js/935f2afb.8ed04548.js new file mode 100644 index 000000000..8a282e5ab --- /dev/null +++ b/assets/js/935f2afb.8ed04548.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"v5","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"Introduction":[{"type":"link","label":"Centrifugo introduction","href":"/docs/getting-started/introduction","docId":"getting-started/introduction","unlisted":false},{"type":"link","label":"Join community","href":"/docs/getting-started/community","docId":"getting-started/community","unlisted":false},{"type":"link","label":"Install Centrifugo","href":"/docs/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quickstart tutorial","href":"/docs/getting-started/quickstart","docId":"getting-started/quickstart","unlisted":false},{"type":"link","label":"Main highlights","href":"/docs/getting-started/highlights","docId":"getting-started/highlights","unlisted":false},{"type":"link","label":"Integration guide","href":"/docs/getting-started/integration","docId":"getting-started/integration","unlisted":false},{"type":"link","label":"Design overview","href":"/docs/getting-started/design","docId":"getting-started/design","unlisted":false},{"type":"link","label":"Ecosystem notes","href":"/docs/getting-started/ecosystem","docId":"getting-started/ecosystem","unlisted":false},{"type":"link","label":"Comparing with others","href":"/docs/getting-started/comparisons","docId":"getting-started/comparisons","unlisted":false},{"type":"link","label":"Migrating to v5","href":"/docs/getting-started/migration_v5","docId":"getting-started/migration_v5","unlisted":false}],"Tutorial":[{"type":"link","label":"Real-time app from scratch","href":"/docs/tutorial/intro","docId":"tutorial/intro","unlisted":false},{"type":"link","label":"App layout and behavior","href":"/docs/tutorial/layout","docId":"tutorial/layout","unlisted":false},{"type":"link","label":"Setting up backend and database","href":"/docs/tutorial/backend","docId":"tutorial/backend","unlisted":false},{"type":"link","label":"Adding reverse proxy","href":"/docs/tutorial/reverse_proxy","docId":"tutorial/reverse_proxy","unlisted":false},{"type":"link","label":"Creating SPA frontend","href":"/docs/tutorial/frontend","docId":"tutorial/frontend","unlisted":false},{"type":"link","label":"Integrating Centrifugo","href":"/docs/tutorial/centrifugo","docId":"tutorial/centrifugo","unlisted":false},{"type":"link","label":"Missed messages recovery","href":"/docs/tutorial/recovery","docId":"tutorial/recovery","unlisted":false},{"type":"link","label":"Broadcast: outbox and CDC","href":"/docs/tutorial/outbox_cdc","docId":"tutorial/outbox_cdc","unlisted":false},{"type":"link","label":"Scale to 100k room members","href":"/docs/tutorial/scale","docId":"tutorial/scale","unlisted":false},{"type":"link","label":"Wrapping up \u2013 things learnt","href":"/docs/tutorial/outro","docId":"tutorial/outro","unlisted":false},{"type":"link","label":"Appx #1: possible improvements","href":"/docs/tutorial/improvements","docId":"tutorial/improvements","unlisted":false},{"type":"link","label":"Appx #2: tips and tricks","href":"/docs/tutorial/tips_and_tricks","docId":"tutorial/tips_and_tricks","unlisted":false}],"Guides":[{"type":"link","label":"Configure Centrifugo","href":"/docs/server/configuration","docId":"server/configuration","unlisted":false},{"type":"link","label":"Server API walkthrough","href":"/docs/server/server_api","docId":"server/server_api","unlisted":false},{"type":"link","label":"Client JWT authentication","href":"/docs/server/authentication","docId":"server/authentication","unlisted":false},{"type":"link","label":"Channels and namespaces","href":"/docs/server/channels","docId":"server/channels","unlisted":false},{"type":"link","label":"Channel permission model","href":"/docs/server/channel_permissions","docId":"server/channel_permissions","unlisted":false},{"type":"link","label":"Channel JWT authorization","href":"/docs/server/channel_token_auth","docId":"server/channel_token_auth","unlisted":false},{"type":"link","label":"Server-side subscriptions","href":"/docs/server/server_subs","docId":"server/server_subs","unlisted":false},{"type":"link","label":"Engines and scalability","href":"/docs/server/engines","docId":"server/engines","unlisted":false},{"type":"link","label":"Async consumers","href":"/docs/server/consumers","docId":"server/consumers","unlisted":false},{"type":"link","label":"History and recovery","href":"/docs/server/history_and_recovery","docId":"server/history_and_recovery","unlisted":false},{"type":"link","label":"Online presence","href":"/docs/server/presence","docId":"server/presence","unlisted":false},{"type":"link","label":"Proxy events to the backend","href":"/docs/server/proxy","docId":"server/proxy","unlisted":false},{"type":"link","label":"Proxy subscription streams","href":"/docs/server/proxy_streams","docId":"server/proxy_streams","unlisted":false},{"type":"link","label":"Admin web UI","href":"/docs/server/admin_web","docId":"server/admin_web","unlisted":false},{"type":"link","label":"Server observability","href":"/docs/server/observability","docId":"server/observability","unlisted":false},{"type":"link","label":"Infrastructure tuning","href":"/docs/server/infra_tuning","docId":"server/infra_tuning","unlisted":false},{"type":"link","label":"Load balancing","href":"/docs/server/load_balancing","docId":"server/load_balancing","unlisted":false},{"type":"link","label":"Configure TLS","href":"/docs/server/tls","docId":"server/tls","unlisted":false},{"type":"link","label":"Error and disconnect codes","href":"/docs/server/codes","docId":"server/codes","unlisted":false},{"type":"link","label":"Helper CLI commands","href":"/docs/server/console_commands","docId":"server/console_commands","unlisted":false}],"Transports":[{"type":"link","label":"Real-time transports","href":"/docs/transports/overview","docId":"transports/overview","unlisted":false},{"type":"category","label":"Bidirectional","collapsed":false,"items":[{"type":"link","label":"Client SDK API","href":"/docs/transports/client_api","docId":"transports/client_api","unlisted":false},{"type":"link","label":"Client real-time SDKs","href":"/docs/transports/client_sdk","docId":"transports/client_sdk","unlisted":false},{"type":"link","label":"WebSocket","href":"/docs/transports/websocket","docId":"transports/websocket","unlisted":false},{"type":"link","label":"HTTP streaming","href":"/docs/transports/http_stream","docId":"transports/http_stream","unlisted":false},{"type":"link","label":"SSE (EventSource)","href":"/docs/transports/sse","docId":"transports/sse","unlisted":false},{"type":"link","label":"SockJS","href":"/docs/transports/sockjs","docId":"transports/sockjs","unlisted":false},{"type":"link","label":"WebTransport","href":"/docs/transports/webtransport","docId":"transports/webtransport","unlisted":false},{"type":"link","label":"Client protocol","href":"/docs/transports/client_protocol","docId":"transports/client_protocol","unlisted":false}],"collapsible":true},{"type":"category","label":"Unidirectional","collapsed":false,"items":[{"type":"link","label":"Unidirectional protocol","href":"/docs/transports/uni_client_protocol","docId":"transports/uni_client_protocol","unlisted":false},{"type":"link","label":"WebSocket","href":"/docs/transports/uni_websocket","docId":"transports/uni_websocket","unlisted":false},{"type":"link","label":"HTTP streaming","href":"/docs/transports/uni_http_stream","docId":"transports/uni_http_stream","unlisted":false},{"type":"link","label":"SSE (EventSource)","href":"/docs/transports/uni_sse","docId":"transports/uni_sse","unlisted":false},{"type":"link","label":"GRPC","href":"/docs/transports/uni_grpc","docId":"transports/uni_grpc","unlisted":false}],"collapsible":true}],"Pro":[{"type":"link","label":"Centrifugo PRO","href":"/docs/pro/overview","docId":"pro/overview","unlisted":false},{"type":"link","label":"Install and run PRO version","href":"/docs/pro/install_and_run","docId":"pro/install_and_run","unlisted":false},{"type":"category","label":"PRO version features","collapsed":false,"items":[{"type":"link","label":"User and channel tracing","href":"/docs/pro/tracing","docId":"pro/tracing","unlisted":false},{"type":"link","label":"Analytics with ClickHouse","href":"/docs/pro/analytics","docId":"pro/analytics","unlisted":false},{"type":"link","label":"Operation rate limits","href":"/docs/pro/rate_limiting","docId":"pro/rate_limiting","unlisted":false},{"type":"link","label":"Push notification API","href":"/docs/pro/push_notifications","docId":"pro/push_notifications","unlisted":false},{"type":"link","label":"SSO for admin UI (OIDC)","href":"/docs/pro/admin_idp_auth","docId":"pro/admin_idp_auth","unlisted":false},{"type":"link","label":"User status API","href":"/docs/pro/user_status","docId":"pro/user_status","unlisted":false},{"type":"link","label":"Connections API","href":"/docs/pro/connections","docId":"pro/connections","unlisted":false},{"type":"link","label":"User blocking API","href":"/docs/pro/user_block","docId":"pro/user_block","unlisted":false},{"type":"link","label":"Token revocation API","href":"/docs/pro/token_revocation","docId":"pro/token_revocation","unlisted":false},{"type":"link","label":"Channel state events","href":"/docs/pro/channel_state_events","docId":"pro/channel_state_events","unlisted":false},{"type":"link","label":"Channel capabilities","href":"/docs/pro/capabilities","docId":"pro/capabilities","unlisted":false},{"type":"link","label":"Channel patterns","href":"/docs/pro/channel_patterns","docId":"pro/channel_patterns","unlisted":false},{"type":"link","label":"Channel CEL expressions","href":"/docs/pro/cel_expressions","docId":"pro/cel_expressions","unlisted":false},{"type":"link","label":"Faster performance","href":"/docs/pro/performance","docId":"pro/performance","unlisted":false},{"type":"link","label":"Singleflight","href":"/docs/pro/singleflight","docId":"pro/singleflight","unlisted":false},{"type":"link","label":"Message batching control","href":"/docs/pro/client_message_batching","docId":"pro/client_message_batching","unlisted":false},{"type":"link","label":"Observability enhancements","href":"/docs/pro/observability_enhancements","docId":"pro/observability_enhancements","unlisted":false},{"type":"link","label":"CPU and RSS stats","href":"/docs/pro/process_stats","docId":"pro/process_stats","unlisted":false}],"collapsible":true}]},"docs":{"attributions":{"id":"attributions","title":"Attributions","description":"Landing Page Images"},"faq/faq_index":{"id":"faq/faq_index","title":"Frequently Asked Questions","description":"Answers to popular questions here."},"flow_diagrams":{"id":"flow_diagrams","title":"flow_diagrams","description":"For swimlanes.io:"},"getting-started/community":{"id":"getting-started/community","title":"Join community","description":"If you find Centrifugo interesting, you are welcome to join our community rooms on Telegram (the most active) and Discord:","sidebar":"Introduction"},"getting-started/comparisons":{"id":"getting-started/comparisons","title":"Comparing with others","description":"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.","sidebar":"Introduction"},"getting-started/design":{"id":"getting-started/design","title":"Design overview","description":"Let\'s discuss some architectural and design topics about Centrifugo.","sidebar":"Introduction"},"getting-started/ecosystem":{"id":"getting-started/ecosystem","title":"Ecosystem notes","description":"Some additional notes about our ecosystem which may help you develop with our tech.","sidebar":"Introduction"},"getting-started/highlights":{"id":"getting-started/highlights","title":"Main highlights","description":"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 \u2728highlights\u2728 here. Every point is then extended throughout the documentation.","sidebar":"Introduction"},"getting-started/installation":{"id":"getting-started/installation","title":"Install Centrifugo","description":"Centrifugo server is written in the Go language. It\'s open-source software, and the source code is available on Github.","sidebar":"Introduction"},"getting-started/integration":{"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.","sidebar":"Introduction"},"getting-started/introduction":{"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.","sidebar":"Introduction"},"getting-started/migration_v4":{"id":"getting-started/migration_v4","title":"Migrating to v4","description":"Centrifugo v4 development was concentrated around two main things:"},"getting-started/migration_v5":{"id":"getting-started/migration_v5","title":"Migrating to v5","description":"Centrifugo v5 migration from v4 should be smooth for most of the use cases.","sidebar":"Introduction"},"getting-started/quickstart":{"id":"getting-started/quickstart","title":"Quickstart tutorial \u23f1\ufe0f","description":"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.","sidebar":"Introduction"},"pro/admin_idp_auth":{"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","sidebar":"Pro"},"pro/analytics":{"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.","sidebar":"Pro"},"pro/capabilities":{"id":"pro/capabilities","title":"Channel capabilities","description":"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.","sidebar":"Pro"},"pro/cel_expressions":{"id":"pro/cel_expressions","title":"Channel CEL expressions","description":"Centrifugo PRO supports CEL expressions (Common Expression Language) for checking channel operation permissions.","sidebar":"Pro"},"pro/channel_patterns":{"id":"pro/channel_patterns","title":"Channel patterns","description":"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.","sidebar":"Pro"},"pro/channel_state_events":{"id":"pro/channel_state_events","title":"Channel state events","description":"Centrifugo PRO has a feature to enable channel state event webhooks to be sent to your configured backend endpoint:","sidebar":"Pro"},"pro/client_message_batching":{"id":"pro/client_message_batching","title":"Message batching control","description":"Centrifugo PRO provides advanced options to tweak connection message write behaviour.","sidebar":"Pro"},"pro/connections":{"id":"pro/connections","title":"Connections API","description":"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.","sidebar":"Pro"},"pro/distributed_rate_limit":{"id":"pro/distributed_rate_limit","title":"Distributed rate limit API","description":"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."},"pro/install_and_run":{"id":"pro/install_and_run","title":"Install and run PRO version","description":"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.","sidebar":"Pro"},"pro/observability_enhancements":{"id":"pro/observability_enhancements","title":"Observability enhancements","description":"Centrifugo PRO has some enhancements to exposed metrics. At this moment it provides channel namespace resolution to the following metrics:","sidebar":"Pro"},"pro/overview":{"id":"pro/overview","title":"Centrifugo PRO","description":"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It\'s packed with a set of unique features offering exceptional benefits to corporate and enterprise environments. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), SSO integrations for admin UI, and more.","sidebar":"Pro"},"pro/performance":{"id":"pro/performance","title":"Faster performance","description":"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further.","sidebar":"Pro"},"pro/process_stats":{"id":"pro/process_stats","title":"CPU and RSS stats","description":"A useful addition of Centrifugo PRO is an ability to show CPU and RSS memory usage of each node in admin web UI.","sidebar":"Pro"},"pro/push_notifications":{"id":"pro/push_notifications","title":"Push notification API","description":"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.","sidebar":"Pro"},"pro/rate_limiting":{"id":"pro/rate_limiting","title":"Operation rate limits","description":"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.","sidebar":"Pro"},"pro/singleflight":{"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.","sidebar":"Pro"},"pro/token_revocation":{"id":"pro/token_revocation","title":"Token revocation API","description":"One more protective instrument in Centrifugo PRO is API to manage token revocations.","sidebar":"Pro"},"pro/tracing":{"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.","sidebar":"Pro"},"pro/user_block":{"id":"pro/user_block","title":"User blocking API","description":"One additional instrument for making protective actions in Centrifugo PRO is user blocking API which allows blocking a specific user on Centrifugo level.","sidebar":"Pro"},"pro/user_status":{"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.","sidebar":"Pro"},"server/admin_web":{"id":"server/admin_web","title":"Admin web UI","description":"Centrifugo comes with a built-in administrative web interface. It enables users to:","sidebar":"Guides"},"server/authentication":{"id":"server/authentication","title":"Client JWT authentication","description":"To authenticate an incoming connection (client), Centrifugo can use a JSON Web Token (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism.","sidebar":"Guides"},"server/channel_permissions":{"id":"server/channel_permissions","title":"Channel permission model","description":"When using Centrifugo server API you don\'t need to think about channel permissions at all \u2013 everything is allowed. In server API case, request to Centrifugo must be issued by your application backend \u2013 so you have all the power to check any required permissions before issuing API request to Centrifugo.","sidebar":"Guides"},"server/channel_token_auth":{"id":"server/channel_token_auth","title":"Channel JWT authorization","description":"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.","sidebar":"Guides"},"server/channels":{"id":"server/channels","title":"Channels and namespaces","description":"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.","sidebar":"Guides"},"server/codes":{"id":"server/codes","title":"Error and disconnect codes","description":"This chapter describes error and disconnect codes Centrifugo uses in a client protocol, also error codes which a server API can return in response.","sidebar":"Guides"},"server/configuration":{"id":"server/configuration","title":"Configure Centrifugo","description":"Let\'s look at how Centrifugo can be configured.","sidebar":"Guides"},"server/console_commands":{"id":"server/console_commands","title":"Helper CLI commands","description":"Here is a list of helpful command-line commands that come with Centrifugo executable.","sidebar":"Guides"},"server/consumers":{"id":"server/consumers","title":"Built-in API command async consumers","description":"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.","sidebar":"Guides"},"server/engines":{"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.","sidebar":"Guides"},"server/history_and_recovery":{"id":"server/history_and_recovery","title":"History and recovery","description":"Centrifugo engines can maintain publication history for channels with configured history size and TTL.","sidebar":"Guides"},"server/infra_tuning":{"id":"server/infra_tuning","title":"Infrastructure tuning","description":"As Centrifugo deals with lots of persistent connections your operating system and server infrastructure must be ready for it.","sidebar":"Guides"},"server/load_balancing":{"id":"server/load_balancing","title":"Load balancing","description":"This chapter shows how to deal with persistent connection load balancing.","sidebar":"Guides"},"server/monitoring":{"id":"server/monitoring","title":"Metrics monitoring","description":"Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite."},"server/observability":{"id":"server/observability","title":"Server observability","description":"To provide a better server observability Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.","sidebar":"Guides"},"server/presence":{"id":"server/presence","title":"Online presence","description":"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it.","sidebar":"Guides"},"server/proxy":{"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.","sidebar":"Guides"},"server/proxy_streams":{"id":"server/proxy_streams","title":"Proxy subscription streams","description":"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.","sidebar":"Guides"},"server/server_api":{"id":"server/server_api","title":"Server API walkthrough","description":"Server API provides different methods to interact with Centrifugo. Specifically, in most cases this is an entry point for publications into channels coming from your application backend. There are two kinds of server API available at the moment:","sidebar":"Guides"},"server/server_subs":{"id":"server/server_subs","title":"Server-side subscriptions","description":"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.","sidebar":"Guides"},"server/tls":{"id":"server/tls","title":"Configure TLS","description":"TLS/SSL layer is very important not only for securing your connections but also to increase a","sidebar":"Guides"},"transports/client_api":{"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.","sidebar":"Transports"},"transports/client_protocol":{"id":"transports/client_protocol","title":"Client protocol","description":"This chapter describes the core concepts of Centrifugo bidirectional client protocol \u2013 concentrating on framing level. If you want to find out details about exposed client API then look at client API document.","sidebar":"Transports"},"transports/client_sdk":{"id":"transports/client_sdk","title":"Client real-time SDKs","description":"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.","sidebar":"Transports"},"transports/http_stream":{"id":"transports/http_stream","title":"HTTP streaming, with bidirectional emulation","description":"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.","sidebar":"Transports"},"transports/overview":{"id":"transports/overview","title":"Real-time transports","description":"Centrifugo supports a variety of transports to deliver real-time messages to clients.","sidebar":"Transports"},"transports/sockjs":{"id":"transports/sockjs","title":"SockJS","description":"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.","sidebar":"Transports"},"transports/sse":{"id":"transports/sse","title":"SSE (EventSource), with bidirectional emulation","description":"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.","sidebar":"Transports"},"transports/uni_client_protocol":{"id":"transports/uni_client_protocol","title":"Unidirectional client protocol","description":"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.","sidebar":"Transports"},"transports/uni_grpc":{"id":"transports/uni_grpc","title":"Unidirectional GRPC","description":"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.","sidebar":"Transports"},"transports/uni_http_stream":{"id":"transports/uni_http_stream","title":"Unidirectional HTTP streaming","description":"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.","sidebar":"Transports"},"transports/uni_sse":{"id":"transports/uni_sse","title":"Unidirectional SSE (EventSource)","description":"Server-Sent Events or EventSource is a well-known HTTP-based transport available in all modern browsers and loved by many developers.","sidebar":"Transports"},"transports/uni_websocket":{"id":"transports/uni_websocket","title":"Unidirectional WebSocket","description":"Default unidirectional WebSocket connection endpoint in Centrifugo is:","sidebar":"Transports"},"transports/websocket":{"id":"transports/websocket","title":"WebSocket","description":"Websocket is the main transport in Centrifugo. It\'s a very efficient low-overhead protocol on top of TCP.","sidebar":"Transports"},"transports/webtransport":{"id":"transports/webtransport","title":"WebTransport","description":"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.","sidebar":"Transports"},"tutorial/backend":{"id":"tutorial/backend","title":"Setting up backend and database","description":"Let\'s start building the app. As the first step, create a directory for the new app:","sidebar":"Tutorial"},"tutorial/centrifugo":{"id":"tutorial/centrifugo","title":"Integrating Centrifugo for real-time event delivery","description":"It\'s finally time for the real-time! In some cases you already have an application and when integrating Centrifugo you start from here.","sidebar":"Tutorial"},"tutorial/frontend":{"id":"tutorial/frontend","title":"Creating SPA frontend with React","description":"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.","sidebar":"Tutorial"},"tutorial/improvements":{"id":"tutorial/improvements","title":"Appendix #1: Possible Improvements","description":"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:","sidebar":"Tutorial"},"tutorial/intro":{"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.","sidebar":"Tutorial"},"tutorial/layout":{"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.","sidebar":"Tutorial"},"tutorial/outbox_cdc":{"id":"tutorial/outbox_cdc","title":"Broadcast using transactional outbox and CDC","description":"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.","sidebar":"Tutorial"},"tutorial/outro":{"id":"tutorial/outro","title":"Wrapping up \u2013 things learnt","description":"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:","sidebar":"Tutorial"},"tutorial/recovery":{"id":"tutorial/recovery","title":"Missed messages recovery","description":"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.","sidebar":"Tutorial"},"tutorial/reverse_proxy":{"id":"tutorial/reverse_proxy","title":"Adding Nginx as a reverse proxy","description":"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.","sidebar":"Tutorial"},"tutorial/scale":{"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","sidebar":"Tutorial"},"tutorial/tips_and_tricks":{"id":"tutorial/tips_and_tricks","title":"Appendix #2: Tips and tricks","description":"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.","sidebar":"Tutorial"}}}')}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.df48e7e3.js b/assets/js/935f2afb.df48e7e3.js deleted file mode 100644 index 863e969f1..000000000 --- a/assets/js/935f2afb.df48e7e3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"v5","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"Introduction":[{"type":"link","label":"Centrifugo introduction","href":"/docs/getting-started/introduction","docId":"getting-started/introduction","unlisted":false},{"type":"link","label":"Join community","href":"/docs/getting-started/community","docId":"getting-started/community","unlisted":false},{"type":"link","label":"Install Centrifugo","href":"/docs/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quickstart tutorial","href":"/docs/getting-started/quickstart","docId":"getting-started/quickstart","unlisted":false},{"type":"link","label":"Main highlights","href":"/docs/getting-started/highlights","docId":"getting-started/highlights","unlisted":false},{"type":"link","label":"Integration guide","href":"/docs/getting-started/integration","docId":"getting-started/integration","unlisted":false},{"type":"link","label":"Design overview","href":"/docs/getting-started/design","docId":"getting-started/design","unlisted":false},{"type":"link","label":"Ecosystem notes","href":"/docs/getting-started/ecosystem","docId":"getting-started/ecosystem","unlisted":false},{"type":"link","label":"Comparing with others","href":"/docs/getting-started/comparisons","docId":"getting-started/comparisons","unlisted":false},{"type":"link","label":"Migrating to v5","href":"/docs/getting-started/migration_v5","docId":"getting-started/migration_v5","unlisted":false}],"Tutorial":[{"type":"link","label":"Real-time app from scratch","href":"/docs/tutorial/intro","docId":"tutorial/intro","unlisted":false},{"type":"link","label":"App layout and behavior","href":"/docs/tutorial/layout","docId":"tutorial/layout","unlisted":false},{"type":"link","label":"Setting up backend and database","href":"/docs/tutorial/backend","docId":"tutorial/backend","unlisted":false},{"type":"link","label":"Adding reverse proxy","href":"/docs/tutorial/reverse_proxy","docId":"tutorial/reverse_proxy","unlisted":false},{"type":"link","label":"Creating SPA frontend","href":"/docs/tutorial/frontend","docId":"tutorial/frontend","unlisted":false},{"type":"link","label":"Integrating Centrifugo","href":"/docs/tutorial/centrifugo","docId":"tutorial/centrifugo","unlisted":false},{"type":"link","label":"Missed messages recovery","href":"/docs/tutorial/recovery","docId":"tutorial/recovery","unlisted":false},{"type":"link","label":"Broadcast: outbox and CDC","href":"/docs/tutorial/outbox_cdc","docId":"tutorial/outbox_cdc","unlisted":false},{"type":"link","label":"Scale to 100k room members","href":"/docs/tutorial/scale","docId":"tutorial/scale","unlisted":false},{"type":"link","label":"Wrapping up \u2013 things learnt","href":"/docs/tutorial/outro","docId":"tutorial/outro","unlisted":false},{"type":"link","label":"Appx #1: possible improvements","href":"/docs/tutorial/improvements","docId":"tutorial/improvements","unlisted":false},{"type":"link","label":"Appx #2: tips and tricks","href":"/docs/tutorial/tips_and_tricks","docId":"tutorial/tips_and_tricks","unlisted":false}],"Guides":[{"type":"link","label":"Configure Centrifugo","href":"/docs/server/configuration","docId":"server/configuration","unlisted":false},{"type":"link","label":"Server API walkthrough","href":"/docs/server/server_api","docId":"server/server_api","unlisted":false},{"type":"link","label":"Client JWT authentication","href":"/docs/server/authentication","docId":"server/authentication","unlisted":false},{"type":"link","label":"Channels and namespaces","href":"/docs/server/channels","docId":"server/channels","unlisted":false},{"type":"link","label":"Channel permission model","href":"/docs/server/channel_permissions","docId":"server/channel_permissions","unlisted":false},{"type":"link","label":"Channel JWT authorization","href":"/docs/server/channel_token_auth","docId":"server/channel_token_auth","unlisted":false},{"type":"link","label":"Server-side subscriptions","href":"/docs/server/server_subs","docId":"server/server_subs","unlisted":false},{"type":"link","label":"Engines and scalability","href":"/docs/server/engines","docId":"server/engines","unlisted":false},{"type":"link","label":"Async consumers","href":"/docs/server/consumers","docId":"server/consumers","unlisted":false},{"type":"link","label":"History and recovery","href":"/docs/server/history_and_recovery","docId":"server/history_and_recovery","unlisted":false},{"type":"link","label":"Online presence","href":"/docs/server/presence","docId":"server/presence","unlisted":false},{"type":"link","label":"Proxy events to the backend","href":"/docs/server/proxy","docId":"server/proxy","unlisted":false},{"type":"link","label":"Proxy subscription streams","href":"/docs/server/proxy_streams","docId":"server/proxy_streams","unlisted":false},{"type":"link","label":"Admin web UI","href":"/docs/server/admin_web","docId":"server/admin_web","unlisted":false},{"type":"link","label":"Server observability","href":"/docs/server/observability","docId":"server/observability","unlisted":false},{"type":"link","label":"Infrastructure tuning","href":"/docs/server/infra_tuning","docId":"server/infra_tuning","unlisted":false},{"type":"link","label":"Load balancing","href":"/docs/server/load_balancing","docId":"server/load_balancing","unlisted":false},{"type":"link","label":"Configure TLS","href":"/docs/server/tls","docId":"server/tls","unlisted":false},{"type":"link","label":"Error and disconnect codes","href":"/docs/server/codes","docId":"server/codes","unlisted":false},{"type":"link","label":"Helper CLI commands","href":"/docs/server/console_commands","docId":"server/console_commands","unlisted":false}],"Transports":[{"type":"link","label":"Real-time transports","href":"/docs/transports/overview","docId":"transports/overview","unlisted":false},{"type":"category","label":"Bidirectional","collapsed":false,"items":[{"type":"link","label":"Client SDK API","href":"/docs/transports/client_api","docId":"transports/client_api","unlisted":false},{"type":"link","label":"Client real-time SDKs","href":"/docs/transports/client_sdk","docId":"transports/client_sdk","unlisted":false},{"type":"link","label":"WebSocket","href":"/docs/transports/websocket","docId":"transports/websocket","unlisted":false},{"type":"link","label":"HTTP streaming","href":"/docs/transports/http_stream","docId":"transports/http_stream","unlisted":false},{"type":"link","label":"SSE (EventSource)","href":"/docs/transports/sse","docId":"transports/sse","unlisted":false},{"type":"link","label":"SockJS","href":"/docs/transports/sockjs","docId":"transports/sockjs","unlisted":false},{"type":"link","label":"WebTransport","href":"/docs/transports/webtransport","docId":"transports/webtransport","unlisted":false},{"type":"link","label":"Client protocol","href":"/docs/transports/client_protocol","docId":"transports/client_protocol","unlisted":false}],"collapsible":true},{"type":"category","label":"Unidirectional","collapsed":false,"items":[{"type":"link","label":"Unidirectional protocol","href":"/docs/transports/uni_client_protocol","docId":"transports/uni_client_protocol","unlisted":false},{"type":"link","label":"WebSocket","href":"/docs/transports/uni_websocket","docId":"transports/uni_websocket","unlisted":false},{"type":"link","label":"HTTP streaming","href":"/docs/transports/uni_http_stream","docId":"transports/uni_http_stream","unlisted":false},{"type":"link","label":"SSE (EventSource)","href":"/docs/transports/uni_sse","docId":"transports/uni_sse","unlisted":false},{"type":"link","label":"GRPC","href":"/docs/transports/uni_grpc","docId":"transports/uni_grpc","unlisted":false}],"collapsible":true}],"Pro":[{"type":"link","label":"Centrifugo PRO","href":"/docs/pro/overview","docId":"pro/overview","unlisted":false},{"type":"link","label":"Install and run PRO version","href":"/docs/pro/install_and_run","docId":"pro/install_and_run","unlisted":false},{"type":"category","label":"PRO version features","collapsed":false,"items":[{"type":"link","label":"User and channel tracing","href":"/docs/pro/tracing","docId":"pro/tracing","unlisted":false},{"type":"link","label":"Analytics with ClickHouse","href":"/docs/pro/analytics","docId":"pro/analytics","unlisted":false},{"type":"link","label":"Operation rate limits","href":"/docs/pro/rate_limiting","docId":"pro/rate_limiting","unlisted":false},{"type":"link","label":"Push notification API","href":"/docs/pro/push_notifications","docId":"pro/push_notifications","unlisted":false},{"type":"link","label":"User status API","href":"/docs/pro/user_status","docId":"pro/user_status","unlisted":false},{"type":"link","label":"Connections API","href":"/docs/pro/connections","docId":"pro/connections","unlisted":false},{"type":"link","label":"User blocking API","href":"/docs/pro/user_block","docId":"pro/user_block","unlisted":false},{"type":"link","label":"Token revocation API","href":"/docs/pro/token_revocation","docId":"pro/token_revocation","unlisted":false},{"type":"link","label":"Channel state events","href":"/docs/pro/channel_state_events","docId":"pro/channel_state_events","unlisted":false},{"type":"link","label":"Channel capabilities","href":"/docs/pro/capabilities","docId":"pro/capabilities","unlisted":false},{"type":"link","label":"Channel patterns","href":"/docs/pro/channel_patterns","docId":"pro/channel_patterns","unlisted":false},{"type":"link","label":"CEL expressions","href":"/docs/pro/cel_expressions","docId":"pro/cel_expressions","unlisted":false},{"type":"link","label":"Faster performance","href":"/docs/pro/performance","docId":"pro/performance","unlisted":false},{"type":"link","label":"Singleflight","href":"/docs/pro/singleflight","docId":"pro/singleflight","unlisted":false},{"type":"link","label":"Message batching control","href":"/docs/pro/client_message_batching","docId":"pro/client_message_batching","unlisted":false},{"type":"link","label":"Observability enhancements","href":"/docs/pro/observability_enhancements","docId":"pro/observability_enhancements","unlisted":false},{"type":"link","label":"CPU and RSS stats","href":"/docs/pro/process_stats","docId":"pro/process_stats","unlisted":false}],"collapsible":true}]},"docs":{"attributions":{"id":"attributions","title":"Attributions","description":"Landing Page Images"},"faq/faq_index":{"id":"faq/faq_index","title":"Frequently Asked Questions","description":"Answers to popular questions here."},"flow_diagrams":{"id":"flow_diagrams","title":"flow_diagrams","description":"For swimlanes.io:"},"getting-started/client_api":{"id":"getting-started/client_api","title":"Client API showcase","description":"This chapter showcases the capabilities of Centrifugo\'s bidirectional client API \u2013 i.e., the real-time messaging primitives available on the front end (which can be a browser or a mobile device)."},"getting-started/community":{"id":"getting-started/community","title":"Join community","description":"If you find Centrifugo interesting, you are welcome to join our community rooms on Telegram (the most active) and Discord:","sidebar":"Introduction"},"getting-started/comparisons":{"id":"getting-started/comparisons","title":"Comparing with others","description":"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.","sidebar":"Introduction"},"getting-started/design":{"id":"getting-started/design","title":"Design overview","description":"Let\'s discuss some architectural and design topics about Centrifugo.","sidebar":"Introduction"},"getting-started/ecosystem":{"id":"getting-started/ecosystem","title":"Ecosystem notes","description":"Some additional notes about our ecosystem which may help you develop with our tech.","sidebar":"Introduction"},"getting-started/highlights":{"id":"getting-started/highlights","title":"Main highlights","description":"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 \u2728highlights\u2728 here. Every point is then extended throughout the documentation.","sidebar":"Introduction"},"getting-started/installation":{"id":"getting-started/installation","title":"Install Centrifugo","description":"Centrifugo server is written in the Go language. It\'s open-source software, and the source code is available on Github.","sidebar":"Introduction"},"getting-started/integration":{"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.","sidebar":"Introduction"},"getting-started/introduction":{"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.","sidebar":"Introduction"},"getting-started/migration_v4":{"id":"getting-started/migration_v4","title":"Migrating to v4","description":"Centrifugo v4 development was concentrated around two main things:"},"getting-started/migration_v5":{"id":"getting-started/migration_v5","title":"Migrating to v5","description":"Centrifugo v5 migration from v4 should be smooth for most of the use cases.","sidebar":"Introduction"},"getting-started/quickstart":{"id":"getting-started/quickstart","title":"Quickstart tutorial \u23f1\ufe0f","description":"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.","sidebar":"Introduction"},"pro/analytics":{"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.","sidebar":"Pro"},"pro/capabilities":{"id":"pro/capabilities","title":"Channel capabilities","description":"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.","sidebar":"Pro"},"pro/cel_expressions":{"id":"pro/cel_expressions","title":"CEL expressions","description":"Centrifugo PRO supports CEL expressions (Common Expression Language) for checking channel operation permissions.","sidebar":"Pro"},"pro/channel_patterns":{"id":"pro/channel_patterns","title":"Channel patterns","description":"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.","sidebar":"Pro"},"pro/channel_state_events":{"id":"pro/channel_state_events","title":"Channel state events","description":"Centrifugo PRO has a feature to enable channel state event webhooks to be sent to your configured backend endpoint:","sidebar":"Pro"},"pro/client_message_batching":{"id":"pro/client_message_batching","title":"Message batching control","description":"Centrifugo PRO provides advanced options to tweak connection message write behaviour.","sidebar":"Pro"},"pro/connections":{"id":"pro/connections","title":"Connections API","description":"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.","sidebar":"Pro"},"pro/distributed_rate_limit":{"id":"pro/distributed_rate_limit","title":"Distributed rate limit API","description":"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."},"pro/install_and_run":{"id":"pro/install_and_run","title":"Install and run PRO version","description":"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.","sidebar":"Pro"},"pro/observability_enhancements":{"id":"pro/observability_enhancements","title":"Observability enhancements","description":"Centrifugo PRO has some enhancements to exposed metrics. At this moment it provides channel namespace resolution to the following metrics:","sidebar":"Pro"},"pro/overview":{"id":"pro/overview","title":"Centrifugo PRO","description":"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It\'s packed with a set of unique features that offer exceptional benefits to your business. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), and more.","sidebar":"Pro"},"pro/performance":{"id":"pro/performance","title":"Faster performance","description":"Centrifugo PRO has performance improvements for several server parts. These improvements can help to reduce tail end-to-end latencies in the application, increase server throughput and/or reduce CPU usage on server machines. Our open-source version has a decent performance by itself, with PRO improvements Cenrifugo steps even further.","sidebar":"Pro"},"pro/process_stats":{"id":"pro/process_stats","title":"CPU and RSS stats","description":"A useful addition of Centrifugo PRO is an ability to show CPU and RSS memory usage of each node in admin web UI.","sidebar":"Pro"},"pro/push_notifications":{"id":"pro/push_notifications","title":"Push notification API","description":"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.","sidebar":"Pro"},"pro/rate_limiting":{"id":"pro/rate_limiting","title":"Operation rate limits","description":"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.","sidebar":"Pro"},"pro/singleflight":{"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.","sidebar":"Pro"},"pro/token_revocation":{"id":"pro/token_revocation","title":"Token revocation API","description":"One more protective instrument in Centrifugo PRO is API to manage token revocations.","sidebar":"Pro"},"pro/tracing":{"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.","sidebar":"Pro"},"pro/user_block":{"id":"pro/user_block","title":"User blocking API","description":"One additional instrument for making protective actions in Centrifugo PRO is user blocking API which allows blocking a specific user on Centrifugo level.","sidebar":"Pro"},"pro/user_status":{"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.","sidebar":"Pro"},"server/admin_web":{"id":"server/admin_web","title":"Admin web UI","description":"Centrifugo comes with a built-in administrative web interface. It enables users to:","sidebar":"Guides"},"server/authentication":{"id":"server/authentication","title":"Client JWT authentication","description":"To authenticate an incoming connection (client), Centrifugo can use a JSON Web Token (JWT) provided by your application backend to the client-side. This allows Centrifugo to identify the user ID within your application in a secure way. Also, the application can pass additional data to Centrifugo inside JWT claims. This chapter explains this authentication mechanism.","sidebar":"Guides"},"server/channel_permissions":{"id":"server/channel_permissions","title":"Channel permission model","description":"When using Centrifugo server API you don\'t need to think about channel permissions at all \u2013 everything is allowed. In server API case, request to Centrifugo must be issued by your application backend \u2013 so you have all the power to check any required permissions before issuing API request to Centrifugo.","sidebar":"Guides"},"server/channel_token_auth":{"id":"server/channel_token_auth","title":"Channel JWT authorization","description":"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.","sidebar":"Guides"},"server/channels":{"id":"server/channels","title":"Channels and namespaces","description":"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.","sidebar":"Guides"},"server/codes":{"id":"server/codes","title":"Error and disconnect codes","description":"This chapter describes error and disconnect codes Centrifugo uses in a client protocol, also error codes which a server API can return in response.","sidebar":"Guides"},"server/configuration":{"id":"server/configuration","title":"Configure Centrifugo","description":"Let\'s look at how Centrifugo can be configured.","sidebar":"Guides"},"server/console_commands":{"id":"server/console_commands","title":"Helper CLI commands","description":"Here is a list of helpful command-line commands that come with Centrifugo executable.","sidebar":"Guides"},"server/consumers":{"id":"server/consumers","title":"Built-in API command async consumers","description":"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.","sidebar":"Guides"},"server/engines":{"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.","sidebar":"Guides"},"server/history_and_recovery":{"id":"server/history_and_recovery","title":"History and recovery","description":"Centrifugo engines can maintain publication history for channels with configured history size and TTL.","sidebar":"Guides"},"server/infra_tuning":{"id":"server/infra_tuning","title":"Infrastructure tuning","description":"As Centrifugo deals with lots of persistent connections your operating system and server infrastructure must be ready for it.","sidebar":"Guides"},"server/load_balancing":{"id":"server/load_balancing","title":"Load balancing","description":"This chapter shows how to deal with persistent connection load balancing.","sidebar":"Guides"},"server/monitoring":{"id":"server/monitoring","title":"Metrics monitoring","description":"Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite."},"server/observability":{"id":"server/observability","title":"Server observability","description":"To provide a better server observability Centrifugo supports reporting metrics in Prometheus format and can automatically export metrics to Graphite.","sidebar":"Guides"},"server/presence":{"id":"server/presence","title":"Online presence","description":"The online presence feature of Centrifugo is a powerful tool that allows you to monitor and manage active users inside a specific channel. It provides an instantaneous snapshot of users currently subscribed to a specific channel. Additionally, Centrifugo may emit join and leave events when clients subscribe to channel and unsubscribe from it.","sidebar":"Guides"},"server/proxy":{"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.","sidebar":"Guides"},"server/proxy_streams":{"id":"server/proxy_streams","title":"Proxy subscription streams","description":"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.","sidebar":"Guides"},"server/server_api":{"id":"server/server_api","title":"Server API walkthrough","description":"Server API provides different methods to interact with Centrifugo. Specifically, in most cases this is an entry point for publications into channels coming from your application backend. There are two kinds of server API available at the moment:","sidebar":"Guides"},"server/server_subs":{"id":"server/server_subs","title":"Server-side subscriptions","description":"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.","sidebar":"Guides"},"server/tls":{"id":"server/tls","title":"Configure TLS","description":"TLS/SSL layer is very important not only for securing your connections but also to increase a","sidebar":"Guides"},"transports/client_api":{"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.","sidebar":"Transports"},"transports/client_protocol":{"id":"transports/client_protocol","title":"Client protocol","description":"This chapter describes the core concepts of Centrifugo bidirectional client protocol \u2013 concentrating on framing level. If you want to find out details about exposed client API then look at client API document.","sidebar":"Transports"},"transports/client_sdk":{"id":"transports/client_sdk","title":"Client real-time SDKs","description":"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.","sidebar":"Transports"},"transports/http_stream":{"id":"transports/http_stream","title":"HTTP streaming, with bidirectional emulation","description":"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.","sidebar":"Transports"},"transports/overview":{"id":"transports/overview","title":"Real-time transports","description":"Centrifugo supports a variety of transports to deliver real-time messages to clients.","sidebar":"Transports"},"transports/sockjs":{"id":"transports/sockjs","title":"SockJS","description":"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.","sidebar":"Transports"},"transports/sse":{"id":"transports/sse","title":"SSE (EventSource), with bidirectional emulation","description":"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.","sidebar":"Transports"},"transports/uni_client_protocol":{"id":"transports/uni_client_protocol","title":"Unidirectional client protocol","description":"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.","sidebar":"Transports"},"transports/uni_grpc":{"id":"transports/uni_grpc","title":"Unidirectional GRPC","description":"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.","sidebar":"Transports"},"transports/uni_http_stream":{"id":"transports/uni_http_stream","title":"Unidirectional HTTP streaming","description":"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.","sidebar":"Transports"},"transports/uni_sse":{"id":"transports/uni_sse","title":"Unidirectional SSE (EventSource)","description":"Server-Sent Events or EventSource is a well-known HTTP-based transport available in all modern browsers and loved by many developers.","sidebar":"Transports"},"transports/uni_websocket":{"id":"transports/uni_websocket","title":"Unidirectional WebSocket","description":"Default unidirectional WebSocket connection endpoint in Centrifugo is:","sidebar":"Transports"},"transports/websocket":{"id":"transports/websocket","title":"WebSocket","description":"Websocket is the main transport in Centrifugo. It\'s a very efficient low-overhead protocol on top of TCP.","sidebar":"Transports"},"transports/webtransport":{"id":"transports/webtransport","title":"WebTransport","description":"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.","sidebar":"Transports"},"tutorial/backend":{"id":"tutorial/backend","title":"Setting up backend and database","description":"Let\'s start building the app. As the first step, create a directory for the new app:","sidebar":"Tutorial"},"tutorial/centrifugo":{"id":"tutorial/centrifugo","title":"Integrating Centrifugo for real-time event delivery","description":"It\'s finally time for the real-time! In some cases you already have an application and when integrating Centrifugo you start from here.","sidebar":"Tutorial"},"tutorial/frontend":{"id":"tutorial/frontend","title":"Creating SPA frontend with React","description":"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.","sidebar":"Tutorial"},"tutorial/improvements":{"id":"tutorial/improvements","title":"Appendix #1: Possible Improvements","description":"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:","sidebar":"Tutorial"},"tutorial/intro":{"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.","sidebar":"Tutorial"},"tutorial/layout":{"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.","sidebar":"Tutorial"},"tutorial/outbox_cdc":{"id":"tutorial/outbox_cdc","title":"Broadcast using transactional outbox and CDC","description":"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.","sidebar":"Tutorial"},"tutorial/outro":{"id":"tutorial/outro","title":"Wrapping up \u2013 things learnt","description":"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:","sidebar":"Tutorial"},"tutorial/recovery":{"id":"tutorial/recovery","title":"Missed messages recovery","description":"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.","sidebar":"Tutorial"},"tutorial/reverse_proxy":{"id":"tutorial/reverse_proxy","title":"Adding Nginx as a reverse proxy","description":"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.","sidebar":"Tutorial"},"tutorial/scale":{"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","sidebar":"Tutorial"},"tutorial/tips_and_tricks":{"id":"tutorial/tips_and_tricks","title":"Appendix #2: Tips and tricks","description":"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.","sidebar":"Tutorial"}}}')}}]); \ No newline at end of file diff --git a/assets/js/9dd8a0d2.cad9bb75.js b/assets/js/9dd8a0d2.116951e9.js similarity index 89% rename from assets/js/9dd8a0d2.cad9bb75.js rename to assets/js/9dd8a0d2.116951e9.js index 07ea9f136..f72dfd37d 100644 --- a/assets/js/9dd8a0d2.cad9bb75.js +++ b/assets/js/9dd8a0d2.116951e9.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[7054,8951,2278,9523,4968,2438,9217],{59250:(e,t,s)=>{s.r(t),s.d(t,{default:()=>c});s(67294);var i=s(86010);const n={featureTitle:"featureTitle_fCfN",featureContent:"featureContent_FT24",featureContentReversed:"featureContentReversed_juV3",featureImage:"featureImage_lJJP",darkSection:"darkSection_dAst",featureImageReversed:"featureImageReversed_n2Vd"};var r=s(85893);function c(e){let{reversed:t,title:s,img:c,text:a,isDark:l}=e;const o=(0,r.jsx)("div",{className:(0,i.Z)("col col--6",n.featureImage,t?n.featureImageReversed:""),children:c}),h=(0,r.jsxs)("div",{className:(0,i.Z)("col col--6",n.featureContent,t?n.featureContentReversed:""),children:[(0,r.jsx)("h3",{className:n.featureTitle,children:s}),a]});return(0,r.jsx)("section",{className:(0,i.Z)("highlightSection",l?n.darkSection+" darkSection":""),children:(0,r.jsx)("div",{className:"container",children:(0,r.jsx)("div",{className:"row",children:t?(0,r.jsxs)(r.Fragment,{children:[h,o]}):(0,r.jsxs)(r.Fragment,{children:[o,h]})})})})}},53225:(e,t,s)=>{s.r(t),s.d(t,{default:()=>d});var i=s(67294),n=s(85893);function r(e,t){return Math.floor(Math.random()*(t-e+1)+e)}function c(e,t,s,i){const n=i*Math.PI/180;return[e+s*Math.cos(n),t+s*Math.sin(n)]}function a(e,t,s,i){return 180*Math.atan2(i-t,s-e)/Math.PI}function l(e,t,s,i,n,r,c,a,l,o,h,d){this.ctx=e,this.init(t,s,i,n,r,c,a,l,o,h,d)}function o(e,t,s,i,n,r){this.ctx=e,this.init(t,s,i,n,r)}let h;if(l.prototype.init=function(e,t,s,i,n,r,c,a,l,o,h){this.X=e,this.Y=t,this.radius=n,this.x=s,this.y=i,this.r=r,this.w=c,this.c=h,this.rotate=a,this.speed=60*l,this.angleDiff=o,this.a=0},l.prototype.drawSegment=function(e,t,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 i=c(this.x,this.y,this.r,e),n=i[0],r=i[1],l=c(this.x,this.y,this.r,t),o=l[0],h=l[1],d=n-this.w,u=h-this.w,m=a(this.x,this.y,d,r),f=a(this.x,this.y,o,u),v=t*Math.PI/180,p=e*Math.PI/180,x=m*Math.PI/180,C=f*Math.PI/180;this.ctx.arc(this.x,this.y,this.r,v,p,!0),this.ctx.arc(this.x,this.y,this.r-this.w,x,C,!1),this.ctx.closePath(),this.ctx.fillStyle=this.c,this.ctx.fill(),this.ctx.stroke()},l.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()},l.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},l.prototype.updateParams=function(e){this.a+=this.speed*e*this.radius/this.r},l.prototype.render=function(e){this.updateParams(e),this.draw()},o.prototype.init=function(e,t,s,i,n){this.X=e,this.Y=t,this.x=s,this.y=i,this.c=n,this.lw=1,this.v={x:100*Math.random(),y:100*Math.random()}},o.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()},o.prototype.updatePosition=function(e){this.x+=this.v.x*e,this.y+=this.v.y*e},o.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)},o.prototype.render=function(e){this.updatePosition(e),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){h=new MutationObserver((function(e){e.forEach((function(e){"attributes"==e.type&&window.dispatchEvent(new Event("resized"))}))}));const e=document.querySelector("html");h.observe(e,{attributes:!0})}const d=e=>{const[t,c]=i.useState({x:1,y:1}),a=i.useRef(null),h=()=>{null!==a.current&&(a.current.width=a.current.clientWidth,a.current.height=a.current.clientHeight,c({x:a.current?a.current.clientWidth:0,y:a.current?a.current.clientHeight:0}))};return i.useEffect((()=>h()),[]),(s.g.window||process&&process.browser)&&(i.useEffect((()=>(window.addEventListener("resize",h),()=>window.removeEventListener("resize",h)))),i.useEffect((()=>(window.addEventListener("resized",h),()=>window.removeEventListener("resized",h))))),i.useEffect((()=>{!function(e,t,i,n){const c=e.getContext("2d"),a=t/2,h=i/2;let d,u;n?(d="#8d3838",u="#6e2b2b"):(d="#ffd4d4",u="#ffd4d4");const m=[],f=[],v=i/7,p=v/15,x=s.g.requestAnimationFrame||s.g.mozRequestAnimationFrame||s.g.webkitRequestAnimationFrame||s.g.msRequestAnimationFrame||function(e){setTimeout(e,17)};for(let s=0;s<3;s+=1){const e=new o(c,t,i,r(0,t),r(0,i),d);m.push(e)}f.push(new l(c,t,i,a,h,v,2.65*v,9*p,0,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,90,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,180,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,270,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,45,1.5,2,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,135,1.5,2,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,225,1.5,2,u));let C=0;x((function e(s){const n=(s-C)/1e3;c.clearRect(0,0,t,i);for(let t=0;t {s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 140 40",children:(0,i.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"})})}},50650:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"108",height:"39",stroke:"#000",strokeLinecap:"round",strokeLinejoin:"round",fill:"#fff",fillRule:"evenodd",children:[(0,i.jsx)("defs",{children:(0,i.jsx)("clipPath",{id:"A",children:(0,i.jsx)("path",{d:"M15.7566 4.3862l-.0308.1504-.4803.1427-.6616.2276-.6558.2662c-.4359.1871-.868.3974-1.2904.6346l-1.2036.7561-.1003-.0463c-3.9985-1.5276-7.5494.3106-7.5494.3106-.3241 4.253 1.597 6.9341 1.977 7.4202l-.2642.7966c-.2951.9644-.517 1.9539-.6539 2.9762l-.054.4436C1.0956 20.2894 0 24.0294 0 24.0294c3.0842 3.5471 6.6795 3.767 6.6795 3.767s.0058-.0058.0097-.0077c.4571.8159.9856 1.5932 1.5816 2.3165l.7831.8795c-1.1245 3.2154.1582 5.8907.1582 5.8907 3.4333.1292 5.6881-1.5026 6.1626-1.8787l1.0377.3086c1.0551.272 2.1352.4321 3.2154.4784l.8081.0154h.1312l.0849-.0019.1697-.0058.1678-.0077.0039.0058c1.6164 2.3068 4.4614 2.6328 4.4614 2.6328 2.0233-2.1333 2.139-4.2492 2.139-4.7063h0v-.0309l-.0019-.0636-.0058-.0984c.4244-.2971.8294-.6172 1.2133-.9606a12.606 12.606 0 0 0 2.1043-2.4669l.162-.2566c2.2896.1312 3.904-1.4177 3.904-1.4177-.38-2.3859-1.7398-3.549-2.0233-3.7689h0s-.0116-.0096-.029-.0212l-.027-.0193-.0501-.0328.0347-.4301.0251-.7696-.002-.191-.0019-.0964v-.0483l-.0019-.0655-.0077-.1601-.0116-.2161-.0154-.2063-.0193-.1987-.0232-.1987-.027-.1967-.1485-.7793c-.2392-1.028-.6404-2.004-1.1708-2.8816s-1.1901-1.655-1.9327-2.3089-1.5758-1.1823-2.4496-1.5758-1.7919-.6462-2.7062-.7619c-.4571-.0598-.9123-.0829-1.3636-.0771l-.1678.0038-.0425.0019-.0579.002-.0694.0038-.1678.0116-.1852.0154-.6886.0965c-.9065.1697-1.7629.4976-2.5171.949s-1.41 1.0165-1.9442 1.6568-.9471 1.3463-1.2326 2.0793-.4455 1.4891-.4899 2.2239l-.0116.5478.0039.1351.0058.1465.0212.2624a5.907 5.907 0 0 0 .2045 1.0743c.1986.6886.5188 1.3116.9123 1.8401s.8641.9683 1.3637 1.3097 1.0338.5864 1.5623.7426a5.434 5.434 0 0 0 1.5528.2218l.1851-.0038.0984-.0039.0984-.0058.1581-.0154c.0116 0 .0289-.0039.0444-.0058l.0482-.0058.0964-.0135c.0656-.0077.1216-.0212.1794-.0309l.1736-.0385c.1138-.0251.2238-.0598.3337-.0926.216-.0714.4186-.1582.6076-.2546s.3626-.2064.5246-.3202l.135-.1022a.395.395 0 0 0 .0618-.5594c-.1216-.1485-.3299-.1871-.4957-.0945l-.1254.0656c-.1447.0694-.2951.135-.4552.1871s-.326.0926-.4996.1234l-.2623.0328c-.0444.0058-.0887.0077-.135.0077l-.1331.0039c-.0424 0-.0849-.0019-.1292-.0019l-.1621-.0078s-.027 0-.0058-.0019l-.0173-.0019-.0367-.0039-.0733-.0077-.1446-.0193c-.3877-.054-.7812-.1678-1.1592-.3395s-.7426-.4069-1.0705-.7021-.6134-.648-.8372-1.0492-.38-.8449-.4532-1.3116c-.0367-.2334-.0521-.4745-.0463-.7099l.0096-.1948c0 .0174.0019-.0096.0019-.0115l.002-.0232.0038-.0482.0097-.0964.0559-.382c.1794-1.0164.6886-2.0079 1.4756-2.762.1967-.1871.4089-.3627.6345-.517s.4668-.2932.7176-.4089.513-.2102.7812-.2835.5439-.1196.8236-.1447l.4204-.0173.0946.0019.1138.0038.0713.002c.029 0 0 0 .0135.0019l.029.0019.1138.0077c.3028.0251.6056.0676.9027.1351a6.44 6.44 0 0 1 1.7147.65c1.0802.5979 2.0002 1.5334 2.5653 2.6618a6.08 6.08 0 0 1 .5825 1.7919l.056.4725.0096.1196.0058.1196.0039.1196.0019.1119v.1022l-.0019.1157-.0135.2797-.0502.5169-.081.5092-.1138.5015c-.0829.3337-.1909.6597-.3163.9799-.2527.6403-.5902 1.2479-.9972 1.8092-.8159 1.1207-1.9288 2.0369-3.1942 2.6117-.6326.2854-1.3.4957-1.9867.6095-.3433.0578-.6905.0925-1.0377.1041l-.0636.002h-.056l-.1118.0019h-.1717-.0849c.0483 0-.0077 0-.0058-.0019l-.0347-.002-.5574-.027c-.7426-.0559-1.4775-.1871-2.1892-.3954s-1.4023-.4822-2.0562-.8294c-1.3058-.6982-2.4727-1.6549-3.387-2.8084-.4591-.5748-.8603-1.192-1.192-1.842s-.5922-1.3309-.7851-2.0272-.3105-1.41-.3568-2.1275l-.0077-.135-.0019-.0328v-.029l-.0019-.0597-.0039-.1177-.0019-.0289v-.0405-.083l-.002-.1678v-.0328c0 .0058 0 .0058 0-.0115v-.0656l.0058-.2623.0888-1.0841.1813-1.0955.2642-1.0763c.2025-.7079.4552-1.3946.7542-2.0426.5998-1.2982 1.3868-2.4419 2.3319-3.3639l.7349-.6539c.2546-.2045.5189-.3935.7908-.5709s.5497-.3414.8371-.4919l.436-.216.2218-.0984.2237-.0945c.299-.1273.6076-.2334.9162-.3337l.2334-.0714.2353-.0655.4745-.1216.2392-.052.2411-.0502.2411-.0443.1215-.0213.1215-.0192.2431-.0367.2739-.0366.2719-.0328.1717-.0174.1138-.0116.0578-.0058.0676-.0038.2758-.0174.137-.0096s.0501-.0019.0057-.0019l.029-.002.0578-.0019.2353-.0116h.9297c.6153.0251 1.2191.0926 1.8054.2006 1.1747.218 2.2799.596 3.279 1.0898s1.898 1.0917 2.6773 1.7456l.1446.1234.1408.1254.2759.2527.2661.2565.2546.2604.9124 1.0609c.5536.7117.9953 1.4292 1.3482 2.1082l.0656.1273.0617.1273.1196.2488.1138.245.1042.2392.3491.9065.3723 1.2383a.304.304 0 0 0 .3221.2276c.1543-.0135.2719-.1408.2758-.2951.0077-.3877-.0019-.8448-.0463-1.3656-.0579-.6442-.1678-1.3907-.3858-2.2104s-.5323-1.7167-.9952-2.6483-1.0705-1.898-1.8575-2.8335a14.502 14.502 0 0 0-1.0049-1.0821c.54-2.1487-.6578-4.0119-.6578-4.0119-2.0677-.1293-3.3831.6423-3.8711.9952l-.2469-.1041-1.086-.3935-1.1399-.3163-1.1881-.2295-.2122-.0309C21.7862 1.2055 19.1957 0 19.1957 0c-2.8894 1.8227-3.4391 4.3862-3.4391 4.3862",stroke:"none",strokeLinejoin:"miter",strokeLinecap:"butt",fill:"#000",fillRule:"nonzero",strokeWidth:".0193",transform:"translate(0 .0077)"})})}),(0,i.jsxs)("g",{fill:"#000",stroke:"none",fillRule:"nonzero",children:[(0,i.jsx)("path",{d:"M55.4146 19.4587c-.1404 3.6146-2.9917 6.4302-6.5348 6.4302-3.7396 0-6.5195-3.0275-6.5195-6.6778 0-3.686 3.0096-6.7135 6.6778-6.7135 1.6567 0 3.2776.7122 4.6484 1.9936l-1.0696 1.3172c-1.0517-.9087-2.3153-1.5316-3.5788-1.5316-2.6905 0-4.8986 2.2081-4.8986 4.9343 0 2.7594 2.083 4.8986 4.7378 4.8986 2.3867 0 4.2578-1.7461 4.6305-3.9898h-5.4117v-1.5853h7.3211v.9241zm6.2157-.873h-.9981c-1.1028 0-1.9936.8935-1.9936 1.9937v5.1997h-1.7818v-8.9062h1.4601v.7479c.4799-.4799 1.2457-.7479 2.1009-.7479h1.9221zm9.7053 7.1959h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM69.513 21.968c.4211-1.8099-.9497-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.09-.1864 1.986-1.0339 2.2362-2.1136m5.4116-5.5903v.4978h2.8309v1.5673h-2.8309v7.3389h-1.7639v-9.3504c0-1.9604 1.4065-3.1168 3.1704-3.1168h2.1366l-.7122 1.6746h-1.4218c-.7837 0-1.4091.6228-1.4091 1.3886m12.2707 9.4039h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zm-1.8201-3.8136c.4212-1.8099-.9496-3.4538-2.7364-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.5239 3.0734 3.2955 2.7722 1.0874-.1864 1.986-1.0339 2.2361-2.1136m11.3338-1.4602v5.2713h-1.7817v-5.2713c0-1.1053-.9087-1.9936-1.9936-1.9936-1.1232 0-2.0115.8909-2.0115 1.9936v5.2713h-1.7818v-8.9062h1.478v.7658c.6407-.5693 1.4959-.9088 2.3868-.9088 2.0651.0026 3.7038 1.695 3.7038 3.7779m10.4736 5.2738h-1.5138v-1.1231c-1.1716 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM105.36 21.968c.4211-1.8099-.9496-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.0875-.1864 1.986-1.0339 2.2362-2.1136"}),(0,i.jsx)("path",{d:"M0 0h35.3825v38.4281H0z",transform:"translate(0 -.0077)",clipPath:"url(#A)"})]})]})}},51374:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsxs)("svg",{width:"160",height:"29",viewBox:"0 0 160 29",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,i.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,i.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,i.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,i.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,i.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,i.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,i.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,i.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"})]})}},52537:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"191",height:"55",viewBox:"0 0 191 55",className:"site-brand__logo",children:(0,i.jsxs)("g",{fill:"#000000",children:[(0,i.jsx)("g",{children:(0,i.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,i.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"})]})})}},49884:(e,t,s)=>{s.r(t),s.d(t,{default:()=>L});var i=s(67294),n=s(86010),r=s(7372),c=s(39960),a=s(52263),l=s(44996);const o={heroBanner:"heroBanner_UJJx",hero:"hero_syme",container:"container_czXe",mainTitle:"mainTitle_BcKq",subTitle:"subTitle_opAm",section:"section_rC2D",sectionAlt:"sectionAlt_XiGz",buttons:"buttons_pzbO",logos:"logos_NYVn",features:"features_keug",featureImage:"featureImage_yA8i",heart:"heart_Zeus",quote:"quote_aYQC",responsiveEmbed:"responsiveEmbed_q7kv",lspDemo:"lspDemo_XLVG",playWithIt:"playWithIt_Xc2P"};var h=s(53225),d=s(92949),u=s(59250),m=s(63349),f=s(50650),v=s(51374),p=s(52537);const x="carousel_NhIU",C="testimonial_bdm8",g="quote_WCQh",w="author_w76v",j="switch_Vlgy";var b=s(85893);const y=[{quote:"We use Centrifugo to power real time updates and chat. It's been incredibly easy to use and reliable.",author:"Victor Pontis, Founder at Luma"},{quote:"Centrifugo listed in our tech radar, and new projects will use it by default.",author:"Marko Kevac, Engineering Manager at Badoo"},{quote:"Nine months in production, and we didn't encounter any issue with Centrifugo \u2013 it performed flawlessly!",author:"Kirill, CTO at RabbitX"}];const k=function(){const[e,t]=(0,i.useState)(0);return(0,b.jsxs)("div",{className:x,children:[(0,b.jsx)("button",{className:j,onClick:()=>{t((e=>0===e?y.length-1:e-1))},children:"<"}),(0,b.jsxs)("div",{className:C,children:[(0,b.jsxs)("blockquote",{className:g,children:["\u201c",y[e].quote,"\u201d"]}),(0,b.jsxs)("p",{className:w,children:["- ",y[e].author]})]}),(0,b.jsx)("button",{className:j,onClick:()=>{t((e=>e===y.length-1?0:e+1))},children:">"})]})},M="banner_G2O5",z="bannerTitle_na9p",V="bannerLink_azGU",N="bannerText_WQt1",_=()=>(0,b.jsxs)("div",{className:M,children:[(0,b.jsxs)("h2",{className:z,children:["Using Centrifugo? Check out ",(0,b.jsx)("a",{className:V,href:"/docs/pro/overview",children:"Centrifugo PRO"})]}),(0,b.jsx)("p",{className:N,children:"Unique experience of self-hosted real-time messaging"})]});function S(e){let{imageUrl:t,title:s,children:i}=e;const r=(0,l.Z)(t);return(0,b.jsxs)("div",{className:(0,n.Z)("col col--4",o.feature),children:[r&&(0,b.jsx)("div",{className:"text--center",children:(0,b.jsx)("div",{className:"feature-media",children:(0,b.jsx)("img",{className:o.featureImage,src:r,alt:s})})}),(0,b.jsx)("h2",{className:"text--center",children:s}),(0,b.jsx)("p",{children:i})]})}function H(){const e="dark"==(0,d.I)().colorMode;return(0,b.jsxs)("header",{id:"hero",className:(0,n.Z)("hero hero--primary",o.heroBanner),children:[(0,b.jsx)(h.default,{isDarkTheme:e}),(0,b.jsxs)("div",{className:"container",style:{zIndex:1},children:[(0,b.jsx)("div",{className:o.mainTitle,children:"CENTRIFUGO"}),(0,b.jsx)("div",{className:o.subTitle,children:"Scalable real-time messaging server. Set up once and forever."}),(0,b.jsx)("div",{className:o.buttons,children:(0,b.jsx)(c.Z,{className:(0,n.Z)("button button--outline button--secondary button--lg"),to:(0,l.Z)("docs/getting-started/introduction"),children:"GET STARTED"})})]})]})}const L=function(){const e=(0,a.Z)(),{siteConfig:{tagline:t}={}}=e;return(0,b.jsxs)(r.Z,{title:t,description:"Centrifugo is an open source server designed to help building interactive real-time messaging applications. Think chats, live comments, multiplayer games, streaming metrics etc. Centrifugo provides a variety of real-time transports, scales well and integrates with any application.",children:[(0,b.jsx)(H,{}),(0,b.jsx)(_,{}),(0,b.jsxs)("main",{children:[(0,b.jsx)("section",{className:(0,n.Z)("logos-wrapper",o.logos),children:(0,b.jsx)("div",{className:"container",children:(0,b.jsxs)("div",{className:"row justify-content-center",children:[(0,b.jsx)("div",{className:"col"}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(m.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(f.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(v.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(p.default,{})}),(0,b.jsx)("div",{className:"col"})]})})}),(0,b.jsx)("section",{className:(0,n.Z)("features-wrapper",o.features),children:(0,b.jsx)("div",{className:"container",children:(0,b.jsxs)("div",{className:"row",children:[(0,b.jsxs)(S,{title:"Integrates with everything",imageUrl:"img/feature_integration.png",children:["Centrifugo is a self-hosted service which handles connections over various ",(0,b.jsx)("a",{href:"/docs/transports/overview",children:"transports"})," and provides a simple ",(0,b.jsx)("a",{href:"/docs/server/server_api",children:"publishing API"}),". Centrifugo nicely integrates with any application \u2014 no changes in the existing app architecture required to introduce real-time updates."]}),(0,b.jsxs)(S,{title:"Great performance",imageUrl:"img/feature_performance.png",children:["Centrifugo is written in Go language with some smart optimizations inside. See the description of the test stand with ",(0,b.jsx)("a",{href:"/blog/2020/02/10/million-connections-with-centrifugo",children:"one million WebSocket"})," connections and 30 million delivered messages per minute with hardware comparable to a single modern server machine."]}),(0,b.jsx)(S,{title:"Feature-rich",imageUrl:"img/feature_rich.png",children:"Centrifugo provides flexible authentication, various types of subscriptions, hot channel history, online presence, the ability to proxy connection events to the backend, and much more. It comes with official SDK libraries for both web and mobile development."}),(0,b.jsx)(S,{title:"Out-of-the-box scalability",imageUrl:"img/feature_scalability.png",children:"Built-in Redis, KeyDB, Tarantool engines, or Nats broker make it possible to scale connections across different Centrifugo nodes. So Centrifugo helps you to scale to millions of active connections with reasonable hardware requirements."}),(0,b.jsx)(S,{title:"Used in production",imageUrl:"img/feature_production.png",children:"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."}),(0,b.jsxs)(S,{title:"Centrifugo PRO",imageUrl:"img/feature_pro.png",children:[(0,b.jsx)("a",{href:"/docs/pro/overview",children:"Centrifugo PRO"})," has a set of unique features on top of the OSS version: analytics with ClickHouse, real-time user and channel tracing, operation throttling, faster performance, token extensions, additional APIs (for example, push notification API), and more."]})]})})}),(0,b.jsx)(u.default,{img:(0,b.jsx)("img",{src:"/img/basic_pub_sub.png"}),reversed:!0,isDark:!0,title:"What is real-time messaging?",text:(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("p",{children:"Real-time messaging is used to create interactive applications where events are delivered to online users with minimal delay."}),(0,b.jsx)("p",{children:"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."}),(0,b.jsxs)("p",{children:["Centrifugo is a user facing ",(0,b.jsx)("b",{children:"PUB/SUB"})," server that handles persistent connections over various real-time transports \u2014 ",(0,b.jsx)("b",{children:"WebSocket"}),", HTTP-streaming, SSE (Server-Sent Events), SockJS, WebTransport, GRPC."]})]})}),(0,b.jsx)(k,{}),(0,b.jsx)(u.default,{img:(0,b.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/dzgXph_pRJ0",title:"YouTube video player",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowFullScreen:!0}),title:"Looking for a cool demo?",text:(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("p",{children:"Here is the real-time telemetry streamed from the Assetto Corsa racing simulator to the Grafana dashboard with a help of our WebSocket technologies."}),(0,b.jsxs)("p",{children:["This demonstrates that you can stream ",(0,b.jsx)("b",{children:"60Hz"})," data towards client connections and thus provide instant visual feedback on the state of the system."]}),(0,b.jsx)("div",{className:o.buttons,children:(0,b.jsx)(c.Z,{className:(0,n.Z)("button button--outline button--secondary button--lg",o.getStarted),to:(0,l.Z)("docs/getting-started/introduction"),children:"Impressive? Get Started!"})})]})})]})]})}},86010:(e,t,s)=>{function i(e){var t,s,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;t n});const n=function(){for(var e,t,s=0,n="";s {s.r(t),s.d(t,{default:()=>c});s(67294);var i=s(86010);const n={featureTitle:"featureTitle_fCfN",featureContent:"featureContent_FT24",featureContentReversed:"featureContentReversed_juV3",featureImage:"featureImage_lJJP",darkSection:"darkSection_dAst",featureImageReversed:"featureImageReversed_n2Vd"};var r=s(85893);function c(e){let{reversed:t,title:s,img:c,text:a,isDark:l}=e;const o=(0,r.jsx)("div",{className:(0,i.Z)("col col--6",n.featureImage,t?n.featureImageReversed:""),children:c}),h=(0,r.jsxs)("div",{className:(0,i.Z)("col col--6",n.featureContent,t?n.featureContentReversed:""),children:[(0,r.jsx)("h3",{className:n.featureTitle,children:s}),a]});return(0,r.jsx)("section",{className:(0,i.Z)("highlightSection",l?n.darkSection+" darkSection":""),children:(0,r.jsx)("div",{className:"container",children:(0,r.jsx)("div",{className:"row",children:t?(0,r.jsxs)(r.Fragment,{children:[h,o]}):(0,r.jsxs)(r.Fragment,{children:[o,h]})})})})}},83951:(e,t,s)=>{s.r(t),s.d(t,{default:()=>d});var i=s(67294),n=s(85893);function r(e,t){return Math.floor(Math.random()*(t-e+1)+e)}function c(e,t,s,i){const n=i*Math.PI/180;return[e+s*Math.cos(n),t+s*Math.sin(n)]}function a(e,t,s,i){return 180*Math.atan2(i-t,s-e)/Math.PI}function l(e,t,s,i,n,r,c,a,l,o,h,d){this.ctx=e,this.init(t,s,i,n,r,c,a,l,o,h,d)}function o(e,t,s,i,n,r){this.ctx=e,this.init(t,s,i,n,r)}let h;if(l.prototype.init=function(e,t,s,i,n,r,c,a,l,o,h){this.X=e,this.Y=t,this.radius=n,this.x=s,this.y=i,this.r=r,this.w=c,this.c=h,this.rotate=a,this.speed=60*l,this.angleDiff=o,this.a=0},l.prototype.drawSegment=function(e,t,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 i=c(this.x,this.y,this.r,e),n=i[0],r=i[1],l=c(this.x,this.y,this.r,t),o=l[0],h=l[1],d=n-this.w,u=h-this.w,m=a(this.x,this.y,d,r),f=a(this.x,this.y,o,u),v=t*Math.PI/180,p=e*Math.PI/180,x=m*Math.PI/180,g=f*Math.PI/180;this.ctx.arc(this.x,this.y,this.r,v,p,!0),this.ctx.arc(this.x,this.y,this.r-this.w,x,g,!1),this.ctx.closePath(),this.ctx.fillStyle=this.c,this.ctx.fill(),this.ctx.stroke()},l.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()},l.prototype.resize=function(){this.x=this.X/2,this.y=this.Y/2},l.prototype.updateParams=function(e){this.a+=this.speed*e*this.radius/this.r},l.prototype.render=function(e){this.updateParams(e),this.draw()},o.prototype.init=function(e,t,s,i,n){this.X=e,this.Y=t,this.x=s,this.y=i,this.c=n,this.lw=1,this.v={x:100*Math.random(),y:100*Math.random()}},o.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()},o.prototype.updatePosition=function(e){this.x+=this.v.x*e,this.y+=this.v.y*e},o.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)},o.prototype.render=function(e){this.updatePosition(e),this.wrapPosition(),this.draw()},s.g.window||process&&process.browser){h=new MutationObserver((function(e){e.forEach((function(e){"attributes"==e.type&&window.dispatchEvent(new Event("resized"))}))}));const e=document.querySelector("html");h.observe(e,{attributes:!0})}const d=e=>{const[t,c]=i.useState({x:1,y:1}),a=i.useRef(null),h=()=>{null!==a.current&&(a.current.width=a.current.clientWidth,a.current.height=a.current.clientHeight,c({x:a.current?a.current.clientWidth:0,y:a.current?a.current.clientHeight:0}))};return i.useEffect((()=>h()),[]),(s.g.window||process&&process.browser)&&(i.useEffect((()=>(window.addEventListener("resize",h),()=>window.removeEventListener("resize",h)))),i.useEffect((()=>(window.addEventListener("resized",h),()=>window.removeEventListener("resized",h))))),i.useEffect((()=>{!function(e,t,i,n){const c=e.getContext("2d"),a=t/2,h=i/2;let d,u;n?(d="#8d3838",u="#6e2b2b"):(d="#ffd4d4",u="#ffd4d4");const m=[],f=[],v=i/7,p=v/15,x=s.g.requestAnimationFrame||s.g.mozRequestAnimationFrame||s.g.webkitRequestAnimationFrame||s.g.msRequestAnimationFrame||function(e){setTimeout(e,17)};for(let s=0;s<3;s+=1){const e=new o(c,t,i,r(0,t),r(0,i),d);m.push(e)}f.push(new l(c,t,i,a,h,v,2.65*v,9*p,0,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,90,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,180,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,2.65*v,9*p,270,-1.5,0,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,45,1.5,2,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,135,1.5,2,u)),f.push(new l(c,t,i,a,h,v,1.45*v,8*p,225,1.5,2,u));let g=0;x((function e(s){const n=(s-g)/1e3;c.clearRect(0,0,t,i);for(let t=0;t {s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 140 40",children:(0,i.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"})})}},97322:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"108",height:"39",stroke:"#000",strokeLinecap:"round",strokeLinejoin:"round",fill:"#fff",fillRule:"evenodd",children:[(0,i.jsx)("defs",{children:(0,i.jsx)("clipPath",{id:"A",children:(0,i.jsx)("path",{d:"M15.7566 4.3862l-.0308.1504-.4803.1427-.6616.2276-.6558.2662c-.4359.1871-.868.3974-1.2904.6346l-1.2036.7561-.1003-.0463c-3.9985-1.5276-7.5494.3106-7.5494.3106-.3241 4.253 1.597 6.9341 1.977 7.4202l-.2642.7966c-.2951.9644-.517 1.9539-.6539 2.9762l-.054.4436C1.0956 20.2894 0 24.0294 0 24.0294c3.0842 3.5471 6.6795 3.767 6.6795 3.767s.0058-.0058.0097-.0077c.4571.8159.9856 1.5932 1.5816 2.3165l.7831.8795c-1.1245 3.2154.1582 5.8907.1582 5.8907 3.4333.1292 5.6881-1.5026 6.1626-1.8787l1.0377.3086c1.0551.272 2.1352.4321 3.2154.4784l.8081.0154h.1312l.0849-.0019.1697-.0058.1678-.0077.0039.0058c1.6164 2.3068 4.4614 2.6328 4.4614 2.6328 2.0233-2.1333 2.139-4.2492 2.139-4.7063h0v-.0309l-.0019-.0636-.0058-.0984c.4244-.2971.8294-.6172 1.2133-.9606a12.606 12.606 0 0 0 2.1043-2.4669l.162-.2566c2.2896.1312 3.904-1.4177 3.904-1.4177-.38-2.3859-1.7398-3.549-2.0233-3.7689h0s-.0116-.0096-.029-.0212l-.027-.0193-.0501-.0328.0347-.4301.0251-.7696-.002-.191-.0019-.0964v-.0483l-.0019-.0655-.0077-.1601-.0116-.2161-.0154-.2063-.0193-.1987-.0232-.1987-.027-.1967-.1485-.7793c-.2392-1.028-.6404-2.004-1.1708-2.8816s-1.1901-1.655-1.9327-2.3089-1.5758-1.1823-2.4496-1.5758-1.7919-.6462-2.7062-.7619c-.4571-.0598-.9123-.0829-1.3636-.0771l-.1678.0038-.0425.0019-.0579.002-.0694.0038-.1678.0116-.1852.0154-.6886.0965c-.9065.1697-1.7629.4976-2.5171.949s-1.41 1.0165-1.9442 1.6568-.9471 1.3463-1.2326 2.0793-.4455 1.4891-.4899 2.2239l-.0116.5478.0039.1351.0058.1465.0212.2624a5.907 5.907 0 0 0 .2045 1.0743c.1986.6886.5188 1.3116.9123 1.8401s.8641.9683 1.3637 1.3097 1.0338.5864 1.5623.7426a5.434 5.434 0 0 0 1.5528.2218l.1851-.0038.0984-.0039.0984-.0058.1581-.0154c.0116 0 .0289-.0039.0444-.0058l.0482-.0058.0964-.0135c.0656-.0077.1216-.0212.1794-.0309l.1736-.0385c.1138-.0251.2238-.0598.3337-.0926.216-.0714.4186-.1582.6076-.2546s.3626-.2064.5246-.3202l.135-.1022a.395.395 0 0 0 .0618-.5594c-.1216-.1485-.3299-.1871-.4957-.0945l-.1254.0656c-.1447.0694-.2951.135-.4552.1871s-.326.0926-.4996.1234l-.2623.0328c-.0444.0058-.0887.0077-.135.0077l-.1331.0039c-.0424 0-.0849-.0019-.1292-.0019l-.1621-.0078s-.027 0-.0058-.0019l-.0173-.0019-.0367-.0039-.0733-.0077-.1446-.0193c-.3877-.054-.7812-.1678-1.1592-.3395s-.7426-.4069-1.0705-.7021-.6134-.648-.8372-1.0492-.38-.8449-.4532-1.3116c-.0367-.2334-.0521-.4745-.0463-.7099l.0096-.1948c0 .0174.0019-.0096.0019-.0115l.002-.0232.0038-.0482.0097-.0964.0559-.382c.1794-1.0164.6886-2.0079 1.4756-2.762.1967-.1871.4089-.3627.6345-.517s.4668-.2932.7176-.4089.513-.2102.7812-.2835.5439-.1196.8236-.1447l.4204-.0173.0946.0019.1138.0038.0713.002c.029 0 0 0 .0135.0019l.029.0019.1138.0077c.3028.0251.6056.0676.9027.1351a6.44 6.44 0 0 1 1.7147.65c1.0802.5979 2.0002 1.5334 2.5653 2.6618a6.08 6.08 0 0 1 .5825 1.7919l.056.4725.0096.1196.0058.1196.0039.1196.0019.1119v.1022l-.0019.1157-.0135.2797-.0502.5169-.081.5092-.1138.5015c-.0829.3337-.1909.6597-.3163.9799-.2527.6403-.5902 1.2479-.9972 1.8092-.8159 1.1207-1.9288 2.0369-3.1942 2.6117-.6326.2854-1.3.4957-1.9867.6095-.3433.0578-.6905.0925-1.0377.1041l-.0636.002h-.056l-.1118.0019h-.1717-.0849c.0483 0-.0077 0-.0058-.0019l-.0347-.002-.5574-.027c-.7426-.0559-1.4775-.1871-2.1892-.3954s-1.4023-.4822-2.0562-.8294c-1.3058-.6982-2.4727-1.6549-3.387-2.8084-.4591-.5748-.8603-1.192-1.192-1.842s-.5922-1.3309-.7851-2.0272-.3105-1.41-.3568-2.1275l-.0077-.135-.0019-.0328v-.029l-.0019-.0597-.0039-.1177-.0019-.0289v-.0405-.083l-.002-.1678v-.0328c0 .0058 0 .0058 0-.0115v-.0656l.0058-.2623.0888-1.0841.1813-1.0955.2642-1.0763c.2025-.7079.4552-1.3946.7542-2.0426.5998-1.2982 1.3868-2.4419 2.3319-3.3639l.7349-.6539c.2546-.2045.5189-.3935.7908-.5709s.5497-.3414.8371-.4919l.436-.216.2218-.0984.2237-.0945c.299-.1273.6076-.2334.9162-.3337l.2334-.0714.2353-.0655.4745-.1216.2392-.052.2411-.0502.2411-.0443.1215-.0213.1215-.0192.2431-.0367.2739-.0366.2719-.0328.1717-.0174.1138-.0116.0578-.0058.0676-.0038.2758-.0174.137-.0096s.0501-.0019.0057-.0019l.029-.002.0578-.0019.2353-.0116h.9297c.6153.0251 1.2191.0926 1.8054.2006 1.1747.218 2.2799.596 3.279 1.0898s1.898 1.0917 2.6773 1.7456l.1446.1234.1408.1254.2759.2527.2661.2565.2546.2604.9124 1.0609c.5536.7117.9953 1.4292 1.3482 2.1082l.0656.1273.0617.1273.1196.2488.1138.245.1042.2392.3491.9065.3723 1.2383a.304.304 0 0 0 .3221.2276c.1543-.0135.2719-.1408.2758-.2951.0077-.3877-.0019-.8448-.0463-1.3656-.0579-.6442-.1678-1.3907-.3858-2.2104s-.5323-1.7167-.9952-2.6483-1.0705-1.898-1.8575-2.8335a14.502 14.502 0 0 0-1.0049-1.0821c.54-2.1487-.6578-4.0119-.6578-4.0119-2.0677-.1293-3.3831.6423-3.8711.9952l-.2469-.1041-1.086-.3935-1.1399-.3163-1.1881-.2295-.2122-.0309C21.7862 1.2055 19.1957 0 19.1957 0c-2.8894 1.8227-3.4391 4.3862-3.4391 4.3862",stroke:"none",strokeLinejoin:"miter",strokeLinecap:"butt",fill:"#000",fillRule:"nonzero",strokeWidth:".0193",transform:"translate(0 .0077)"})})}),(0,i.jsxs)("g",{fill:"#000",stroke:"none",fillRule:"nonzero",children:[(0,i.jsx)("path",{d:"M55.4146 19.4587c-.1404 3.6146-2.9917 6.4302-6.5348 6.4302-3.7396 0-6.5195-3.0275-6.5195-6.6778 0-3.686 3.0096-6.7135 6.6778-6.7135 1.6567 0 3.2776.7122 4.6484 1.9936l-1.0696 1.3172c-1.0517-.9087-2.3153-1.5316-3.5788-1.5316-2.6905 0-4.8986 2.2081-4.8986 4.9343 0 2.7594 2.083 4.8986 4.7378 4.8986 2.3867 0 4.2578-1.7461 4.6305-3.9898h-5.4117v-1.5853h7.3211v.9241zm6.2157-.873h-.9981c-1.1028 0-1.9936.8935-1.9936 1.9937v5.1997h-1.7818v-8.9062h1.4601v.7479c.4799-.4799 1.2457-.7479 2.1009-.7479h1.9221zm9.7053 7.1959h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM69.513 21.968c.4211-1.8099-.9497-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.09-.1864 1.986-1.0339 2.2362-2.1136m5.4116-5.5903v.4978h2.8309v1.5673h-2.8309v7.3389h-1.7639v-9.3504c0-1.9604 1.4065-3.1168 3.1704-3.1168h2.1366l-.7122 1.6746h-1.4218c-.7837 0-1.4091.6228-1.4091 1.3886m12.2707 9.4039h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zm-1.8201-3.8136c.4212-1.8099-.9496-3.4538-2.7364-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.5239 3.0734 3.2955 2.7722 1.0874-.1864 1.986-1.0339 2.2361-2.1136m11.3338-1.4602v5.2713h-1.7817v-5.2713c0-1.1053-.9087-1.9936-1.9936-1.9936-1.1232 0-2.0115.8909-2.0115 1.9936v5.2713h-1.7818v-8.9062h1.478v.7658c.6407-.5693 1.4959-.9088 2.3868-.9088 2.0651.0026 3.7038 1.695 3.7038 3.7779m10.4736 5.2738h-1.5138v-1.1231c-1.1716 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM105.36 21.968c.4211-1.8099-.9496-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.0875-.1864 1.986-1.0339 2.2362-2.1136"}),(0,i.jsx)("path",{d:"M0 0h35.3825v38.4281H0z",transform:"translate(0 -.0077)",clipPath:"url(#A)"})]})]})}},66796:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsxs)("svg",{width:"160",height:"29",viewBox:"0 0 160 29",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[(0,i.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,i.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,i.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,i.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,i.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,i.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,i.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,i.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"})]})}},39012:(e,t,s)=>{s.r(t),s.d(t,{default:()=>n});s(67294);var i=s(85893);function n(){return(0,i.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"191",height:"55",viewBox:"0 0 191 55",className:"site-brand__logo",children:(0,i.jsxs)("g",{fill:"#000000",children:[(0,i.jsx)("g",{children:(0,i.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,i.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"})]})})}},10140:(e,t,s)=>{s.r(t),s.d(t,{default:()=>L});var i=s(67294),n=s(86010),r=s(78299),c=s(75013),a=s(6832),l=s(51402);const o={heroBanner:"heroBanner_UJJx",hero:"hero_syme",container:"container_czXe",mainTitle:"mainTitle_BcKq",subTitle:"subTitle_opAm",section:"section_rC2D",sectionAlt:"sectionAlt_XiGz",buttons:"buttons_pzbO",logos:"logos_NYVn",features:"features_keug",featureImage:"featureImage_yA8i",heart:"heart_Zeus",quote:"quote_aYQC",responsiveEmbed:"responsiveEmbed_q7kv",lspDemo:"lspDemo_XLVG",playWithIt:"playWithIt_Xc2P"};var h=s(83951),d=s(70524),u=s(85279),m=s(73857),f=s(97322),v=s(66796),p=s(39012);const x="carousel_NhIU",g="testimonial_bdm8",C="quote_WCQh",w="author_w76v",j="switch_Vlgy";var b=s(85893);const y=[{quote:"We use Centrifugo to power real time updates and chat. It's been incredibly easy to use and reliable.",author:"Victor Pontis, Founder at Luma"},{quote:"Centrifugo listed in our tech radar, and new projects will use it by default.",author:"Marko Kevac, Engineering Manager at Badoo"},{quote:"Nine months in production, and we didn't encounter any issue with Centrifugo \u2013 it performed flawlessly!",author:"Kirill, CTO at RabbitX"}];const k=function(){const[e,t]=(0,i.useState)(0);return(0,b.jsxs)("div",{className:x,children:[(0,b.jsx)("button",{className:j,onClick:()=>{t((e=>0===e?y.length-1:e-1))},children:"<"}),(0,b.jsxs)("div",{className:g,children:[(0,b.jsxs)("blockquote",{className:C,children:["\u201c",y[e].quote,"\u201d"]}),(0,b.jsxs)("p",{className:w,children:["- ",y[e].author]})]}),(0,b.jsx)("button",{className:j,onClick:()=>{t((e=>e===y.length-1?0:e+1))},children:">"})]})},z="banner_G2O5",M="bannerTitle_na9p",V="bannerLink_azGU",N="bannerText_WQt1",_=()=>(0,b.jsxs)("div",{className:z,children:[(0,b.jsxs)("h2",{className:M,children:["Using Centrifugo? Check out ",(0,b.jsx)("a",{className:V,href:"/docs/pro/overview",children:"Centrifugo PRO"})]}),(0,b.jsx)("p",{className:N,children:"Unique experience of self-hosted real-time messaging"})]});function S(e){let{imageUrl:t,title:s,children:i}=e;const r=(0,l.Z)(t);return(0,b.jsxs)("div",{className:(0,n.Z)("col col--4",o.feature),children:[r&&(0,b.jsx)("div",{className:"text--center",children:(0,b.jsx)("div",{className:"feature-media",children:(0,b.jsx)("img",{className:o.featureImage,src:r,alt:s})})}),(0,b.jsx)("h2",{className:"text--center",children:s}),(0,b.jsx)("p",{children:i})]})}function H(){const e="dark"==(0,d.I)().colorMode;return(0,b.jsxs)("header",{id:"hero",className:(0,n.Z)("hero hero--primary",o.heroBanner),children:[(0,b.jsx)(h.default,{isDarkTheme:e}),(0,b.jsxs)("div",{className:"container",style:{zIndex:1},children:[(0,b.jsx)("div",{className:o.mainTitle,children:"CENTRIFUGO"}),(0,b.jsx)("div",{className:o.subTitle,children:"Scalable real-time messaging server. Set up once and forever."}),(0,b.jsx)("div",{className:o.buttons,children:(0,b.jsx)(c.Z,{className:(0,n.Z)("button button--outline button--secondary button--lg"),to:(0,l.Z)("docs/getting-started/introduction"),children:"GET STARTED"})})]})]})}const L=function(){const e=(0,a.Z)(),{siteConfig:{tagline:t}={}}=e;return(0,b.jsxs)(r.Z,{title:t,description:"Centrifugo is an open source server designed to help building interactive real-time messaging applications. Think chats, live comments, multiplayer games, streaming metrics etc. Centrifugo provides a variety of real-time transports, scales well and integrates with any application.",children:[(0,b.jsx)(H,{}),(0,b.jsx)(_,{}),(0,b.jsxs)("main",{children:[(0,b.jsx)("section",{className:(0,n.Z)("logos-wrapper",o.logos),children:(0,b.jsx)("div",{className:"container",children:(0,b.jsxs)("div",{className:"row justify-content-center",children:[(0,b.jsx)("div",{className:"col"}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(m.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(f.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(v.default,{})}),(0,b.jsx)("div",{className:"col",children:(0,b.jsx)(p.default,{})}),(0,b.jsx)("div",{className:"col"})]})})}),(0,b.jsx)("section",{className:(0,n.Z)("features-wrapper",o.features),children:(0,b.jsx)("div",{className:"container",children:(0,b.jsxs)("div",{className:"row",children:[(0,b.jsxs)(S,{title:"Integrates with everything",imageUrl:"img/feature_integration.png",children:["Centrifugo is a self-hosted service which handles connections over various ",(0,b.jsx)("a",{href:"/docs/transports/overview",children:"transports"})," and provides a simple ",(0,b.jsx)("a",{href:"/docs/server/server_api",children:"publishing API"}),". Centrifugo nicely integrates with any application \u2014 no changes in the existing app architecture required to introduce real-time updates."]}),(0,b.jsxs)(S,{title:"Great performance",imageUrl:"img/feature_performance.png",children:["Centrifugo is written in Go language with some smart optimizations inside. See the description of the test stand with ",(0,b.jsx)("a",{href:"/blog/2020/02/10/million-connections-with-centrifugo",children:"one million WebSocket"})," connections and 30 million delivered messages per minute with hardware comparable to a single modern server machine."]}),(0,b.jsx)(S,{title:"Feature-rich",imageUrl:"img/feature_rich.png",children:"Centrifugo provides flexible authentication, various types of subscriptions, hot channel history, online presence, the ability to proxy connection events to the backend, and much more. It comes with official SDK libraries for both web and mobile development."}),(0,b.jsx)(S,{title:"Out-of-the-box scalability",imageUrl:"img/feature_scalability.png",children:"Built-in Redis, KeyDB, Tarantool engines, or Nats broker make it possible to scale connections across different Centrifugo nodes. So Centrifugo helps you to scale to millions of active connections with reasonable hardware requirements."}),(0,b.jsx)(S,{title:"Used in production",imageUrl:"img/feature_production.png",children:"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."}),(0,b.jsxs)(S,{title:"Centrifugo PRO",imageUrl:"img/feature_pro.png",children:[(0,b.jsx)("a",{href:"/docs/pro/overview",children:"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."]})]})})}),(0,b.jsx)(u.default,{img:(0,b.jsx)("img",{src:"/img/basic_pub_sub.png"}),reversed:!0,isDark:!0,title:"What is real-time messaging?",text:(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("p",{children:"Real-time messaging is used to create interactive applications where events are delivered to online users with minimal delay."}),(0,b.jsx)("p",{children:"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."}),(0,b.jsxs)("p",{children:["Centrifugo is a user facing ",(0,b.jsx)("b",{children:"PUB/SUB"})," server that handles persistent connections over various real-time transports \u2014 ",(0,b.jsx)("b",{children:"WebSocket"}),", HTTP-streaming, SSE (Server-Sent Events), SockJS, WebTransport, GRPC."]})]})}),(0,b.jsx)(k,{}),(0,b.jsx)(u.default,{img:(0,b.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/dzgXph_pRJ0",title:"YouTube video player",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",allowFullScreen:!0}),title:"Looking for a cool demo?",text:(0,b.jsxs)(b.Fragment,{children:[(0,b.jsx)("p",{children:"Here is the real-time telemetry streamed from the Assetto Corsa racing simulator to the Grafana dashboard with a help of our WebSocket technologies."}),(0,b.jsxs)("p",{children:["This demonstrates that you can stream ",(0,b.jsx)("b",{children:"60Hz"})," data towards client connections and thus provide instant visual feedback on the state of the system."]}),(0,b.jsx)("div",{className:o.buttons,children:(0,b.jsx)(c.Z,{className:(0,n.Z)("button button--outline button--secondary button--lg",o.getStarted),to:(0,l.Z)("docs/getting-started/introduction"),children:"Impressive? Get Started!"})})]})})]})]})}},86010:(e,t,s)=>{function i(e){var t,s,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;t n});const n=function(){for(var e,t,s=0,n="";s {t.r(r),t.d(r,{default:()=>o});t(67294);var a=t(75013),s=t(11614),i=t(44873),n=t(78299),c=t(34055),l=t(85893);function d(e){let{year:r,posts:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c.Z,{as:"h3",id:r,children:r}),(0,l.jsx)("ul",{children:t.map((e=>(0,l.jsx)("li",{children:(0,l.jsxs)(a.Z,{to:e.metadata.permalink,children:[e.metadata.formattedDate," - ",e.metadata.title]})},e.metadata.date)))})]})}function h(e){let{years:r}=e;return(0,l.jsx)("section",{className:"margin-vert--lg",children:(0,l.jsx)("div",{className:"container",children:(0,l.jsx)("div",{className:"row",children:r.map(((e,r)=>(0,l.jsx)("div",{className:"col col--4 margin-vert--lg",children:(0,l.jsx)(d,{...e})},r)))})})})}function o(e){let{archive:r}=e;const t=(0,s.I)({id:"theme.blog.archive.title",message:"Archive",description:"The page & hero title of the blog archive page"}),a=(0,s.I)({id:"theme.blog.archive.description",message:"Archive",description:"The page & hero description of the blog archive page"}),d=function(e){const r=e.reduce(((e,r)=>{const t=r.metadata.date.split("-")[0],a=e.get(t)??[];return e.set(t,[r,...a])}),new Map);return Array.from(r,(e=>{let[r,t]=e;return{year:r,posts:t}}))}(r.blogPosts);return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(i.d,{title:t,description:a}),(0,l.jsxs)(n.Z,{children:[(0,l.jsx)("header",{className:"hero hero--primary",children:(0,l.jsxs)("div",{className:"container",children:[(0,l.jsx)(c.Z,{as:"h1",className:"hero__title",children:t}),(0,l.jsx)("p",{className:"hero__subtitle",children:a})]})}),(0,l.jsx)("main",{children:d.length>0&&(0,l.jsx)(h,{years:d})})]})]})}}}]); \ No newline at end of file diff --git a/assets/js/9e4087bc.f45b38fc.js b/assets/js/9e4087bc.f45b38fc.js deleted file mode 100644 index 245fd6abe..000000000 --- a/assets/js/9e4087bc.f45b38fc.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3608],{63169:(e,r,t)=>{t.r(r),t.d(r,{default:()=>o});t(67294);var a=t(39960),s=t(95999),i=t(10833),n=t(7372),c=t(92503),l=t(85893);function d(e){let{year:r,posts:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c.Z,{as:"h3",id:r,children:r}),(0,l.jsx)("ul",{children:t.map((e=>(0,l.jsx)("li",{children:(0,l.jsxs)(a.Z,{to:e.metadata.permalink,children:[e.metadata.formattedDate," - ",e.metadata.title]})},e.metadata.date)))})]})}function h(e){let{years:r}=e;return(0,l.jsx)("section",{className:"margin-vert--lg",children:(0,l.jsx)("div",{className:"container",children:(0,l.jsx)("div",{className:"row",children:r.map(((e,r)=>(0,l.jsx)("div",{className:"col col--4 margin-vert--lg",children:(0,l.jsx)(d,{...e})},r)))})})})}function o(e){let{archive:r}=e;const t=(0,s.I)({id:"theme.blog.archive.title",message:"Archive",description:"The page & hero title of the blog archive page"}),a=(0,s.I)({id:"theme.blog.archive.description",message:"Archive",description:"The page & hero description of the blog archive page"}),d=function(e){const r=e.reduce(((e,r)=>{const t=r.metadata.date.split("-")[0],a=e.get(t)??[];return e.set(t,[r,...a])}),new Map);return Array.from(r,(e=>{let[r,t]=e;return{year:r,posts:t}}))}(r.blogPosts);return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(i.d,{title:t,description:a}),(0,l.jsxs)(n.Z,{children:[(0,l.jsx)("header",{className:"hero hero--primary",children:(0,l.jsxs)("div",{className:"container",children:[(0,l.jsx)(c.Z,{as:"h1",className:"hero__title",children:t}),(0,l.jsx)("p",{className:"hero__subtitle",children:a})]})}),(0,l.jsx)("main",{children:d.length>0&&(0,l.jsx)(h,{years:d})})]})]})}}}]); \ No newline at end of file diff --git a/assets/js/a6aa9e1f.3a9e3c48.js b/assets/js/a6aa9e1f.3a9e3c48.js deleted file mode 100644 index 4c15d3093..000000000 --- a/assets/js/a6aa9e1f.3a9e3c48.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3089],{61460:(e,t,i)=>{i.d(t,{Z:()=>v});var s=i(67294),n=i(36905),a=i(7372),r=i(87524),l=i(39960),o=i(95999),c=i(16550),d=i(48596);function m(e){const{pathname:t}=(0,c.TH)();return(0,s.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,d.Mg)(e.permalink,t))}(e,t)))),[e,t])}const g={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var u=i(85893);function h(e){let{sidebar:t}=e;const i=m(t.items);return(0,u.jsx)("aside",{className:"col col--3",children:(0,u.jsxs)("nav",{className:(0,n.Z)(g.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,u.jsx)("div",{className:(0,n.Z)(g.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,u.jsx)("ul",{className:(0,n.Z)(g.sidebarItemList,"clean-list"),children:i.map((e=>(0,u.jsx)("li",{className:g.sidebarItem,children:(0,u.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:g.sidebarItemLink,activeClassName:g.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var p=i(13102);function x(e){let{sidebar:t}=e;const i=m(t.items);return(0,u.jsx)("ul",{className:"menu__list",children:i.map((e=>(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function b(e){return(0,u.jsx)(p.Zo,{component:x,props:e})}function j(e){let{sidebar:t}=e;const i=(0,r.i)();return t?.items.length?"mobile"===i?(0,u.jsx)(b,{sidebar:t}):(0,u.jsx)(h,{sidebar:t}):null}function v(e){const{sidebar:t,toc:i,children:s,...r}=e,l=t&&t.items.length>0;return(0,u.jsx)(a.Z,{...r,children:(0,u.jsx)("div",{className:"container margin-vert--lg",children:(0,u.jsxs)("div",{className:"row",children:[(0,u.jsx)(j,{sidebar:t}),(0,u.jsx)("main",{className:(0,n.Z)("col",{"col--7":l,"col--9 col--offset-1":!l}),itemScope:!0,itemType:"https://schema.org/Blog",children:s}),i&&(0,u.jsx)("div",{className:"col col--2",children:i})]})})})}},80046:(e,t,i)=>{i.r(t),i.d(t,{default:()=>h});i(67294);var s=i(36905),n=i(52263),a=i(10833),r=i(35281),l=i(61460),o=i(99703),c=i(90197),d=i(42426),m=i(85893);function g(e){const{metadata:t}=e,{siteConfig:{title:i}}=(0,n.Z)(),{blogDescription:s,blogTitle:r,permalink:l}=t,o="/"===l?i:r;return(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(a.d,{title:o,description:s}),(0,m.jsx)(c.Z,{tag:"blog_posts_list"})]})}function u(e){const{metadata:t,items:i,sidebar:s}=e;return(0,m.jsxs)(l.Z,{sidebar:s,children:[(0,m.jsx)(d.Z,{items:i}),(0,m.jsx)(o.Z,{metadata:t})]})}function h(e){return(0,m.jsxs)(a.FG,{className:(0,s.Z)(r.k.wrapper.blogPages,r.k.page.blogListPage),children:[(0,m.jsx)(g,{...e}),(0,m.jsx)(u,{...e})]})}},99703:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(95999),n=i(32244),a=i(85893);function r(e){const{metadata:t}=e,{previousPage:i,nextPage:r}=t;return(0,a.jsxs)("nav",{className:"pagination-nav","aria-label":(0,s.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[i&&(0,a.jsx)(n.Z,{permalink:i,title:(0,a.jsx)(s.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),r&&(0,a.jsx)(n.Z,{permalink:r,title:(0,a.jsx)(s.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},15289:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(44996),n=i(9460),a=i(85893);function r(e){let{children:t,className:i}=e;const{frontMatter:r,assets:l,metadata:{description:o}}=(0,n.C)(),{withBaseUrl:c}=(0,s.C)(),d=l.image??r.image,m=r.keywords??[];return(0,a.jsxs)("article",{className:i,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,a.jsx)("meta",{itemProp:"description",content:o}),d&&(0,a.jsx)("link",{itemProp:"image",href:c(d,{absolute:!0})}),m.length>0&&(0,a.jsx)("meta",{itemProp:"keywords",content:m.join(",")}),t]})}},32244:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(36905),n=i(39960),a=i(85893);function r(e){const{permalink:t,title:i,subLabel:r,isNext:l}=e;return(0,a.jsxs)(n.Z,{className:(0,s.Z)("pagination-nav__link",l?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[r&&(0,a.jsx)("div",{className:"pagination-nav__sublabel",children:r}),(0,a.jsx)("div",{className:"pagination-nav__label",children:i})]})}},9460:(e,t,i)=>{i.d(t,{C:()=>o,n:()=>l});var s=i(67294),n=i(902),a=i(85893);const r=s.createContext(null);function l(e){let{children:t,content:i,isBlogPostPage:n=!1}=e;const l=function(e){let{content:t,isBlogPostPage:i}=e;return(0,s.useMemo)((()=>({metadata:t.metadata,frontMatter:t.frontMatter,assets:t.assets,toc:t.toc,isBlogPostPage:i})),[t,i])}({content:i,isBlogPostPage:n});return(0,a.jsx)(r.Provider,{value:l,children:t})}function o(){const e=(0,s.useContext)(r);if(null===e)throw new n.i6("BlogPostProvider");return e}},42426:(e,t,i)=>{i.d(t,{Z:()=>o});i(67294);var s=i(39960),n=i(9460),a=i(15289);const r={container:"container_nU41",leftColumn:"leftColumn_mxRM"};var l=i(85893);function o(e){return(0,l.jsx)(l.Fragment,{children:(0,l.jsx)(c,{...e})})}function c(e){let{items:t,component:i=d}=e;return(0,l.jsx)(l.Fragment,{children:t.map((e=>{let{content:t}=e;return(0,l.jsx)(n.n,{content:t,children:(0,l.jsx)(i,{children:(0,l.jsx)(t,{})})},t.metadata.permalink)}))})}function d(e){let{className:t}=e;const{metadata:i}=(0,n.C)(),{permalink:o,title:c,formattedDate:d,frontMatter:m,description:g}=i,u=i.authors[0];return(0,l.jsx)(a.Z,{className:t,children:(0,l.jsxs)("div",{className:r.container,children:[(0,l.jsx)("div",{className:r.leftColumn,children:(0,l.jsx)("img",{src:m.image,width:"200px"})}),(0,l.jsxs)("div",{className:r.rightColumn,children:[(0,l.jsx)("div",{children:(0,l.jsx)(s.Z,{itemProp:"url",to:o,style:{fontSize:"1.0em"},children:c})}),(0,l.jsxs)("div",{style:{fontSize:"0.8em",color:"#6d6666"},children:[d," by ",u?.name]}),(0,l.jsx)("div",{children:(0,l.jsx)("div",{children:(0,l.jsx)("div",{style:{fontSize:"0.9em"},children:g})})})]})]})})}}}]); \ No newline at end of file diff --git a/assets/js/a6aa9e1f.aedcfb27.js b/assets/js/a6aa9e1f.aedcfb27.js new file mode 100644 index 000000000..d265e828c --- /dev/null +++ b/assets/js/a6aa9e1f.aedcfb27.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[3089],{38762:(e,t,i)=>{i.d(t,{Z:()=>v});var s=i(67294),n=i(36905),a=i(78299),r=i(94980),l=i(75013),o=i(11614),c=i(16550),d=i(18407);function m(e){const{pathname:t}=(0,c.TH)();return(0,s.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,d.Mg)(e.permalink,t))}(e,t)))),[e,t])}const g={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var u=i(85893);function h(e){let{sidebar:t}=e;const i=m(t.items);return(0,u.jsx)("aside",{className:"col col--3",children:(0,u.jsxs)("nav",{className:(0,n.Z)(g.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,u.jsx)("div",{className:(0,n.Z)(g.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,u.jsx)("ul",{className:(0,n.Z)(g.sidebarItemList,"clean-list"),children:i.map((e=>(0,u.jsx)("li",{className:g.sidebarItem,children:(0,u.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:g.sidebarItemLink,activeClassName:g.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var p=i(82306);function x(e){let{sidebar:t}=e;const i=m(t.items);return(0,u.jsx)("ul",{className:"menu__list",children:i.map((e=>(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function b(e){return(0,u.jsx)(p.Zo,{component:x,props:e})}function j(e){let{sidebar:t}=e;const i=(0,r.i)();return t?.items.length?"mobile"===i?(0,u.jsx)(b,{sidebar:t}):(0,u.jsx)(h,{sidebar:t}):null}function v(e){const{sidebar:t,toc:i,children:s,...r}=e,l=t&&t.items.length>0;return(0,u.jsx)(a.Z,{...r,children:(0,u.jsx)("div",{className:"container margin-vert--lg",children:(0,u.jsxs)("div",{className:"row",children:[(0,u.jsx)(j,{sidebar:t}),(0,u.jsx)("main",{className:(0,n.Z)("col",{"col--7":l,"col--9 col--offset-1":!l}),itemScope:!0,itemType:"https://schema.org/Blog",children:s}),i&&(0,u.jsx)("div",{className:"col col--2",children:i})]})})})}},51895:(e,t,i)=>{i.r(t),i.d(t,{default:()=>h});i(67294);var s=i(36905),n=i(6832),a=i(44873),r=i(18015),l=i(38762),o=i(61052),c=i(26145),d=i(78969),m=i(85893);function g(e){const{metadata:t}=e,{siteConfig:{title:i}}=(0,n.Z)(),{blogDescription:s,blogTitle:r,permalink:l}=t,o="/"===l?i:r;return(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(a.d,{title:o,description:s}),(0,m.jsx)(c.Z,{tag:"blog_posts_list"})]})}function u(e){const{metadata:t,items:i,sidebar:s}=e;return(0,m.jsxs)(l.Z,{sidebar:s,children:[(0,m.jsx)(d.Z,{items:i}),(0,m.jsx)(o.Z,{metadata:t})]})}function h(e){return(0,m.jsxs)(a.FG,{className:(0,s.Z)(r.k.wrapper.blogPages,r.k.page.blogListPage),children:[(0,m.jsx)(g,{...e}),(0,m.jsx)(u,{...e})]})}},61052:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(11614),n=i(16948),a=i(85893);function r(e){const{metadata:t}=e,{previousPage:i,nextPage:r}=t;return(0,a.jsxs)("nav",{className:"pagination-nav","aria-label":(0,s.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[i&&(0,a.jsx)(n.Z,{permalink:i,title:(0,a.jsx)(s.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),r&&(0,a.jsx)(n.Z,{permalink:r,title:(0,a.jsx)(s.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},83400:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(51402),n=i(17762),a=i(85893);function r(e){let{children:t,className:i}=e;const{frontMatter:r,assets:l,metadata:{description:o}}=(0,n.C)(),{withBaseUrl:c}=(0,s.C)(),d=l.image??r.image,m=r.keywords??[];return(0,a.jsxs)("article",{className:i,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,a.jsx)("meta",{itemProp:"description",content:o}),d&&(0,a.jsx)("link",{itemProp:"image",href:c(d,{absolute:!0})}),m.length>0&&(0,a.jsx)("meta",{itemProp:"keywords",content:m.join(",")}),t]})}},16948:(e,t,i)=>{i.d(t,{Z:()=>r});i(67294);var s=i(36905),n=i(75013),a=i(85893);function r(e){const{permalink:t,title:i,subLabel:r,isNext:l}=e;return(0,a.jsxs)(n.Z,{className:(0,s.Z)("pagination-nav__link",l?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[r&&(0,a.jsx)("div",{className:"pagination-nav__sublabel",children:r}),(0,a.jsx)("div",{className:"pagination-nav__label",children:i})]})}},17762:(e,t,i)=>{i.d(t,{C:()=>o,n:()=>l});var s=i(67294),n=i(93478),a=i(85893);const r=s.createContext(null);function l(e){let{children:t,content:i,isBlogPostPage:n=!1}=e;const l=function(e){let{content:t,isBlogPostPage:i}=e;return(0,s.useMemo)((()=>({metadata:t.metadata,frontMatter:t.frontMatter,assets:t.assets,toc:t.toc,isBlogPostPage:i})),[t,i])}({content:i,isBlogPostPage:n});return(0,a.jsx)(r.Provider,{value:l,children:t})}function o(){const e=(0,s.useContext)(r);if(null===e)throw new n.i6("BlogPostProvider");return e}},78969:(e,t,i)=>{i.d(t,{Z:()=>o});i(67294);var s=i(75013),n=i(17762),a=i(83400);const r={container:"container_nU41",leftColumn:"leftColumn_mxRM"};var l=i(85893);function o(e){return(0,l.jsx)(l.Fragment,{children:(0,l.jsx)(c,{...e})})}function c(e){let{items:t,component:i=d}=e;return(0,l.jsx)(l.Fragment,{children:t.map((e=>{let{content:t}=e;return(0,l.jsx)(n.n,{content:t,children:(0,l.jsx)(i,{children:(0,l.jsx)(t,{})})},t.metadata.permalink)}))})}function d(e){let{className:t}=e;const{metadata:i}=(0,n.C)(),{permalink:o,title:c,formattedDate:d,frontMatter:m,description:g}=i,u=i.authors[0];return(0,l.jsx)(a.Z,{className:t,children:(0,l.jsxs)("div",{className:r.container,children:[(0,l.jsx)("div",{className:r.leftColumn,children:(0,l.jsx)("img",{src:m.image,width:"200px"})}),(0,l.jsxs)("div",{className:r.rightColumn,children:[(0,l.jsx)("div",{children:(0,l.jsx)(s.Z,{itemProp:"url",to:o,style:{fontSize:"1.0em"},children:c})}),(0,l.jsxs)("div",{style:{fontSize:"0.8em",color:"#6d6666"},children:[d," by ",u?.name]}),(0,l.jsx)("div",{children:(0,l.jsx)("div",{children:(0,l.jsx)("div",{style:{fontSize:"0.9em"},children:g})})})]})]})})}}}]); \ No newline at end of file diff --git a/assets/js/a7bd4aaa.15976e2e.js b/assets/js/a7bd4aaa.2b9e79d7.js similarity index 73% rename from assets/js/a7bd4aaa.15976e2e.js rename to assets/js/a7bd4aaa.2b9e79d7.js index 72c1289dc..f250c06eb 100644 --- a/assets/js/a7bd4aaa.15976e2e.js +++ b/assets/js/a7bd4aaa.2b9e79d7.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8518],{8564:(n,e,s)=>{s.r(e),s.d(e,{default:()=>d});s(67294);var r=s(10833),o=s(43320),t=s(74477),i=s(18790),c=s(90197),u=s(85893);function a(n){const{version:e}=n;return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(c.Z,{version:e.version,tag:(0,o.os)(e.pluginId,e.version)}),(0,u.jsx)(r.d,{children:e.noIndex&&(0,u.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})]})}function l(n){const{version:e,route:s}=n;return(0,u.jsx)(r.FG,{className:e.className,children:(0,u.jsx)(t.q,{version:e,children:(0,i.H)(s.routes)})})}function d(n){return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(a,{...n}),(0,u.jsx)(l,{...n})]})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[8518],{72596:(n,e,s)=>{s.r(e),s.d(e,{default:()=>d});s(67294);var r=s(44873),o=s(39105),t=s(6141),i=s(18790),c=s(26145),u=s(85893);function a(n){const{version:e}=n;return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(c.Z,{version:e.version,tag:(0,o.os)(e.pluginId,e.version)}),(0,u.jsx)(r.d,{children:e.noIndex&&(0,u.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})]})}function l(n){const{version:e,route:s}=n;return(0,u.jsx)(r.FG,{className:e.className,children:(0,u.jsx)(t.q,{version:e,children:(0,i.H)(s.routes)})})}function d(n){return(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(a,{...n}),(0,u.jsx)(l,{...n})]})}}}]); \ No newline at end of file diff --git a/assets/js/a94703ab.59fc5966.js b/assets/js/a94703ab.59fc5966.js deleted file mode 100644 index 0e5d50bf1..000000000 --- a/assets/js/a94703ab.59fc5966.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4368],{12674:(e,t,n)=>{n.r(t),n.d(t,{default:()=>be});var a=n(67294),o=n(36905),i=n(10833),s=n(35281),l=n(53438),r=n(1116),c=n(95999),d=n(12466),u=n(85936);const m={backToTopButton:"backToTopButton_sjWU",backToTopButtonShow:"backToTopButtonShow_xfvO"};var b=n(85893);function h(){const{shown:e,scrollToTop:t}=function(e){let{threshold:t}=e;const[n,o]=(0,a.useState)(!1),i=(0,a.useRef)(!1),{startScroll:s,cancelScroll:l}=(0,d.Ct)();return(0,d.RF)(((e,n)=>{let{scrollY:a}=e;const s=n?.scrollY;s&&(i.current?i.current=!1:a>=s?(l(),o(!1)):a {e.location.hash&&(i.current=!0,o(!1))})),{shown:n,scrollToTop:()=>s(0)}}({threshold:300});return(0,b.jsx)("button",{"aria-label":(0,c.I)({id:"theme.BackToTopButton.buttonAriaLabel",message:"Scroll back to top",description:"The ARIA label for the back to top button"}),className:(0,o.Z)("clean-btn",s.k.common.backToTopButton,m.backToTopButton,e&&m.backToTopButtonShow),type:"button",onClick:t})}var p=n(91442),x=n(16550),f=n(87524),j=n(86668),k=n(21327);function _(e){return(0,b.jsx)("svg",{width:"20",height:"20","aria-hidden":"true",...e,children:(0,b.jsxs)("g",{fill:"#7a7a7a",children:[(0,b.jsx)("path",{d:"M9.992 10.023c0 .2-.062.399-.172.547l-4.996 7.492a.982.982 0 01-.828.454H1c-.55 0-1-.453-1-1 0-.2.059-.403.168-.551l4.629-6.942L.168 3.078A.939.939 0 010 2.528c0-.548.45-.997 1-.997h2.996c.352 0 .649.18.828.45L9.82 9.472c.11.148.172.347.172.55zm0 0"}),(0,b.jsx)("path",{d:"M19.98 10.023c0 .2-.058.399-.168.547l-4.996 7.492a.987.987 0 01-.828.454h-3c-.547 0-.996-.453-.996-1 0-.2.059-.403.168-.551l4.625-6.942-4.625-6.945a.939.939 0 01-.168-.55 1 1 0 01.996-.997h3c.348 0 .649.18.828.45l4.996 7.492c.11.148.168.347.168.55zm0 0"})]})})}const v={collapseSidebarButton:"collapseSidebarButton_PEFL",collapseSidebarButtonIcon:"collapseSidebarButtonIcon_kv0_"};function g(e){let{onClick:t}=e;return(0,b.jsx)("button",{type:"button",title:(0,c.I)({id:"theme.docs.sidebar.collapseButtonTitle",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.collapseButtonAriaLabel",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),className:(0,o.Z)("button button--secondary button--outline",v.collapseSidebarButton),onClick:t,children:(0,b.jsx)(_,{className:v.collapseSidebarButtonIcon})})}var C=n(59689),S=n(902);const I=Symbol("EmptyContext"),N=a.createContext(I);function T(e){let{children:t}=e;const[n,o]=(0,a.useState)(null),i=(0,a.useMemo)((()=>({expandedItem:n,setExpandedItem:o})),[n]);return(0,b.jsx)(N.Provider,{value:i,children:t})}var B=n(86043),Z=n(48596),A=n(39960),L=n(72389);function y(e){let{collapsed:t,categoryLabel:n,onClick:a}=e;return(0,b.jsx)("button",{"aria-label":t?(0,c.I)({id:"theme.DocSidebarItem.expandCategoryAriaLabel",message:"Expand sidebar category '{label}'",description:"The ARIA label to expand the sidebar category"},{label:n}):(0,c.I)({id:"theme.DocSidebarItem.collapseCategoryAriaLabel",message:"Collapse sidebar category '{label}'",description:"The ARIA label to collapse the sidebar category"},{label:n}),type:"button",className:"clean-btn menu__caret",onClick:a})}function w(e){let{item:t,onItemClick:n,activePath:i,level:r,index:c,...d}=e;const{items:u,label:m,collapsible:h,className:p,href:x}=t,{docs:{sidebar:{autoCollapseCategories:f}}}=(0,j.L)(),k=function(e){const t=(0,L.Z)();return(0,a.useMemo)((()=>e.href&&!e.linkUnlisted?e.href:!t&&e.collapsible?(0,l.LM)(e):void 0),[e,t])}(t),_=(0,l._F)(t,i),v=(0,Z.Mg)(x,i),{collapsed:g,setCollapsed:C}=(0,B.u)({initialState:()=>!!h&&(!_&&t.collapsed)}),{expandedItem:T,setExpandedItem:w}=function(){const e=(0,a.useContext)(N);if(e===I)throw new S.i6("DocSidebarItemsExpandedStateProvider");return e}(),E=function(e){void 0===e&&(e=!g),w(e?null:c),C(e)};return function(e){let{isActive:t,collapsed:n,updateCollapsed:o}=e;const i=(0,S.D9)(t);(0,a.useEffect)((()=>{t&&!i&&n&&o(!1)}),[t,i,n,o])}({isActive:_,collapsed:g,updateCollapsed:E}),(0,a.useEffect)((()=>{h&&null!=T&&T!==c&&f&&C(!0)}),[h,T,c,C,f]),(0,b.jsxs)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemCategory,s.k.docs.docSidebarItemCategoryLevel(r),"menu__list-item",{"menu__list-item--collapsed":g},p),children:[(0,b.jsxs)("div",{className:(0,o.Z)("menu__list-item-collapsible",{"menu__list-item-collapsible--active":v}),children:[(0,b.jsx)(A.Z,{className:(0,o.Z)("menu__link",{"menu__link--sublist":h,"menu__link--sublist-caret":!x&&h,"menu__link--active":_}),onClick:h?e=>{n?.(t),x?E(!1):(e.preventDefault(),E())}:()=>{n?.(t)},"aria-current":v?"page":void 0,"aria-expanded":h?!g:void 0,href:h?k??"#":k,...d,children:m}),x&&h&&(0,b.jsx)(y,{collapsed:g,categoryLabel:m,onClick:e=>{e.preventDefault(),E()}})]}),(0,b.jsx)(B.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:g,children:(0,b.jsx)(V,{items:u,tabIndex:g?-1:0,onItemClick:n,activePath:i,level:r+1})})]})}var E=n(13919),H=n(39471);const M={menuExternalLink:"menuExternalLink_NmtK"};function R(e){let{item:t,onItemClick:n,activePath:a,level:i,index:r,...c}=e;const{href:d,label:u,className:m,autoAddBaseUrl:h}=t,p=(0,l._F)(t,a),x=(0,E.Z)(d);return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(i),"menu__list-item",m),children:(0,b.jsxs)(A.Z,{className:(0,o.Z)("menu__link",!x&&M.menuExternalLink,{"menu__link--active":p}),autoAddBaseUrl:h,"aria-current":p?"page":void 0,to:d,...x&&{onClick:n?()=>n(t):void 0},...c,children:[u,!x&&(0,b.jsx)(H.Z,{})]})},u)}const W={menuHtmlItem:"menuHtmlItem_M9Kj"};function F(e){let{item:t,level:n,index:a}=e;const{value:i,defaultStyle:l,className:r}=t;return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(n),l&&[W.menuHtmlItem,"menu__list-item"],r),dangerouslySetInnerHTML:{__html:i}},a)}function P(e){let{item:t,...n}=e;switch(t.type){case"category":return(0,b.jsx)(w,{item:t,...n});case"html":return(0,b.jsx)(F,{item:t,...n});default:return(0,b.jsx)(R,{item:t,...n})}}function D(e){let{items:t,...n}=e;const a=(0,l.f)(t,n.activePath);return(0,b.jsx)(T,{children:a.map(((e,t)=>(0,b.jsx)(P,{item:e,index:t,...n},t)))})}const V=(0,a.memo)(D),U={menu:"menu_SIkG",menuWithAnnouncementBar:"menuWithAnnouncementBar_GW3s"};function K(e){let{path:t,sidebar:n,className:i}=e;const l=function(){const{isActive:e}=(0,C.nT)(),[t,n]=(0,a.useState)(e);return(0,d.RF)((t=>{let{scrollY:a}=t;e&&n(0===a)}),[e]),e&&t}();return(0,b.jsx)("nav",{"aria-label":(0,c.I)({id:"theme.docs.sidebar.navAriaLabel",message:"Docs sidebar",description:"The ARIA label for the sidebar navigation"}),className:(0,o.Z)("menu thin-scrollbar",U.menu,l&&U.menuWithAnnouncementBar,i),children:(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:n,activePath:t,level:1})})})}const Y="sidebar_njMd",z="sidebarWithHideableNavbar_wUlq",G="sidebarHidden_VK0M",O="sidebarLogo_isFc";function q(e){let{path:t,sidebar:n,onCollapse:a,isHidden:i}=e;const{navbar:{hideOnScroll:s},docs:{sidebar:{hideable:l}}}=(0,j.L)();return(0,b.jsxs)("div",{className:(0,o.Z)(Y,s&&z,i&&G),children:[s&&(0,b.jsx)(k.Z,{tabIndex:-1,className:O}),(0,b.jsx)(K,{path:t,sidebar:n}),l&&(0,b.jsx)(g,{onClick:a})]})}const J=a.memo(q);var Q=n(13102),X=n(93163);const $=e=>{let{sidebar:t,path:n}=e;const a=(0,X.e)();return(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:t,activePath:n,onItemClick:e=>{"category"===e.type&&e.href&&a.toggle(),"link"===e.type&&a.toggle()},level:1})})};function ee(e){return(0,b.jsx)(Q.Zo,{component:$,props:e})}const te=a.memo(ee);function ne(e){const t=(0,f.i)(),n="desktop"===t||"ssr"===t,a="mobile"===t;return(0,b.jsxs)(b.Fragment,{children:[n&&(0,b.jsx)(J,{...e}),a&&(0,b.jsx)(te,{...e})]})}const ae={expandButton:"expandButton_TmdG",expandButtonIcon:"expandButtonIcon_i1dp"};function oe(e){let{toggleSidebar:t}=e;return(0,b.jsx)("div",{className:ae.expandButton,title:(0,c.I)({id:"theme.docs.sidebar.expandButtonTitle",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.expandButtonAriaLabel",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),tabIndex:0,role:"button",onKeyDown:t,onClick:t,children:(0,b.jsx)(_,{className:ae.expandButtonIcon})})}const ie={docSidebarContainer:"docSidebarContainer_YfHR",docSidebarContainerHidden:"docSidebarContainerHidden_DPk8",sidebarViewport:"sidebarViewport_aRkj"};function se(e){let{children:t}=e;const n=(0,r.V)();return(0,b.jsx)(a.Fragment,{children:t},n?.name??"noSidebar")}function le(e){let{sidebar:t,hiddenSidebarContainer:n,setHiddenSidebarContainer:i}=e;const{pathname:l}=(0,x.TH)(),[r,c]=(0,a.useState)(!1),d=(0,a.useCallback)((()=>{r&&c(!1),!r&&(0,p.n)()&&c(!0),i((e=>!e))}),[i,r]);return(0,b.jsx)("aside",{className:(0,o.Z)(s.k.docs.docSidebarContainer,ie.docSidebarContainer,n&&ie.docSidebarContainerHidden),onTransitionEnd:e=>{e.currentTarget.classList.contains(ie.docSidebarContainer)&&n&&c(!0)},children:(0,b.jsx)(se,{children:(0,b.jsxs)("div",{className:(0,o.Z)(ie.sidebarViewport,r&&ie.sidebarViewportHidden),children:[(0,b.jsx)(ne,{sidebar:t,path:l,onCollapse:d,isHidden:r}),r&&(0,b.jsx)(oe,{toggleSidebar:d})]})})})}const re={docMainContainer:"docMainContainer_TBSr",docMainContainerEnhanced:"docMainContainerEnhanced_lQrH",docItemWrapperEnhanced:"docItemWrapperEnhanced_JWYK"};function ce(e){let{hiddenSidebarContainer:t,children:n}=e;const a=(0,r.V)();return(0,b.jsx)("main",{className:(0,o.Z)(re.docMainContainer,(t||!a)&&re.docMainContainerEnhanced),children:(0,b.jsx)("div",{className:(0,o.Z)("container padding-top--md padding-bottom--lg",re.docItemWrapper,t&&re.docItemWrapperEnhanced),children:n})})}const de={docRoot:"docRoot_UBD9",docsWrapper:"docsWrapper_hBAB"};function ue(e){let{children:t}=e;const n=(0,r.V)(),[o,i]=(0,a.useState)(!1);return(0,b.jsxs)("div",{className:de.docsWrapper,children:[(0,b.jsx)(h,{}),(0,b.jsxs)("div",{className:de.docRoot,children:[n&&(0,b.jsx)(le,{sidebar:n.items,hiddenSidebarContainer:o,setHiddenSidebarContainer:i}),(0,b.jsx)(ce,{hiddenSidebarContainer:o,children:t})]})]})}var me=n(5658);function be(e){const t=(0,l.SN)(e);if(!t)return(0,b.jsx)(me.Z,{});const{docElement:n,sidebarName:a,sidebarItems:c}=t;return(0,b.jsx)(i.FG,{className:(0,o.Z)(s.k.page.docsDocPage),children:(0,b.jsx)(r.b,{name:a,items:c,children:(0,b.jsx)(ue,{children:n})})})}},5658:(e,t,n)=>{n.d(t,{Z:()=>l});n(67294);var a=n(36905),o=n(95999),i=n(92503),s=n(85893);function l(e){let{className:t}=e;return(0,s.jsx)("main",{className:(0,a.Z)("container margin-vert--xl",t),children:(0,s.jsx)("div",{className:"row",children:(0,s.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,s.jsx)(i.Z,{as:"h1",className:"hero__title",children:(0,s.jsx)(o.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,s.jsx)("p",{children:(0,s.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,s.jsx)("p",{children:(0,s.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."})})]})})})}}}]); \ No newline at end of file diff --git a/assets/js/a94703ab.73548d5e.js b/assets/js/a94703ab.73548d5e.js new file mode 100644 index 000000000..e489bfb24 --- /dev/null +++ b/assets/js/a94703ab.73548d5e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[4368],{43193:(e,t,n)=>{n.r(t),n.d(t,{default:()=>be});var a=n(67294),o=n(36905),i=n(44873),s=n(18015),l=n(85919),r=n(50003),c=n(11614),d=n(63735),u=n(68265);const m={backToTopButton:"backToTopButton_sjWU",backToTopButtonShow:"backToTopButtonShow_xfvO"};var b=n(85893);function h(){const{shown:e,scrollToTop:t}=function(e){let{threshold:t}=e;const[n,o]=(0,a.useState)(!1),i=(0,a.useRef)(!1),{startScroll:s,cancelScroll:l}=(0,d.Ct)();return(0,d.RF)(((e,n)=>{let{scrollY:a}=e;const s=n?.scrollY;s&&(i.current?i.current=!1:a>=s?(l(),o(!1)):a {e.location.hash&&(i.current=!0,o(!1))})),{shown:n,scrollToTop:()=>s(0)}}({threshold:300});return(0,b.jsx)("button",{"aria-label":(0,c.I)({id:"theme.BackToTopButton.buttonAriaLabel",message:"Scroll back to top",description:"The ARIA label for the back to top button"}),className:(0,o.Z)("clean-btn",s.k.common.backToTopButton,m.backToTopButton,e&&m.backToTopButtonShow),type:"button",onClick:t})}var p=n(39657),x=n(16550),f=n(94980),j=n(96793),k=n(49627);function _(e){return(0,b.jsx)("svg",{width:"20",height:"20","aria-hidden":"true",...e,children:(0,b.jsxs)("g",{fill:"#7a7a7a",children:[(0,b.jsx)("path",{d:"M9.992 10.023c0 .2-.062.399-.172.547l-4.996 7.492a.982.982 0 01-.828.454H1c-.55 0-1-.453-1-1 0-.2.059-.403.168-.551l4.629-6.942L.168 3.078A.939.939 0 010 2.528c0-.548.45-.997 1-.997h2.996c.352 0 .649.18.828.45L9.82 9.472c.11.148.172.347.172.55zm0 0"}),(0,b.jsx)("path",{d:"M19.98 10.023c0 .2-.058.399-.168.547l-4.996 7.492a.987.987 0 01-.828.454h-3c-.547 0-.996-.453-.996-1 0-.2.059-.403.168-.551l4.625-6.942-4.625-6.945a.939.939 0 01-.168-.55 1 1 0 01.996-.997h3c.348 0 .649.18.828.45l4.996 7.492c.11.148.168.347.168.55zm0 0"})]})})}const v={collapseSidebarButton:"collapseSidebarButton_PEFL",collapseSidebarButtonIcon:"collapseSidebarButtonIcon_kv0_"};function g(e){let{onClick:t}=e;return(0,b.jsx)("button",{type:"button",title:(0,c.I)({id:"theme.docs.sidebar.collapseButtonTitle",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.collapseButtonAriaLabel",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),className:(0,o.Z)("button button--secondary button--outline",v.collapseSidebarButton),onClick:t,children:(0,b.jsx)(_,{className:v.collapseSidebarButtonIcon})})}var C=n(69061),S=n(93478);const I=Symbol("EmptyContext"),N=a.createContext(I);function T(e){let{children:t}=e;const[n,o]=(0,a.useState)(null),i=(0,a.useMemo)((()=>({expandedItem:n,setExpandedItem:o})),[n]);return(0,b.jsx)(N.Provider,{value:i,children:t})}var B=n(17940),Z=n(18407),A=n(75013),L=n(5730);function y(e){let{collapsed:t,categoryLabel:n,onClick:a}=e;return(0,b.jsx)("button",{"aria-label":t?(0,c.I)({id:"theme.DocSidebarItem.expandCategoryAriaLabel",message:"Expand sidebar category '{label}'",description:"The ARIA label to expand the sidebar category"},{label:n}):(0,c.I)({id:"theme.DocSidebarItem.collapseCategoryAriaLabel",message:"Collapse sidebar category '{label}'",description:"The ARIA label to collapse the sidebar category"},{label:n}),type:"button",className:"clean-btn menu__caret",onClick:a})}function w(e){let{item:t,onItemClick:n,activePath:i,level:r,index:c,...d}=e;const{items:u,label:m,collapsible:h,className:p,href:x}=t,{docs:{sidebar:{autoCollapseCategories:f}}}=(0,j.L)(),k=function(e){const t=(0,L.Z)();return(0,a.useMemo)((()=>e.href&&!e.linkUnlisted?e.href:!t&&e.collapsible?(0,l.LM)(e):void 0),[e,t])}(t),_=(0,l._F)(t,i),v=(0,Z.Mg)(x,i),{collapsed:g,setCollapsed:C}=(0,B.u)({initialState:()=>!!h&&(!_&&t.collapsed)}),{expandedItem:T,setExpandedItem:w}=function(){const e=(0,a.useContext)(N);if(e===I)throw new S.i6("DocSidebarItemsExpandedStateProvider");return e}(),E=function(e){void 0===e&&(e=!g),w(e?null:c),C(e)};return function(e){let{isActive:t,collapsed:n,updateCollapsed:o}=e;const i=(0,S.D9)(t);(0,a.useEffect)((()=>{t&&!i&&n&&o(!1)}),[t,i,n,o])}({isActive:_,collapsed:g,updateCollapsed:E}),(0,a.useEffect)((()=>{h&&null!=T&&T!==c&&f&&C(!0)}),[h,T,c,C,f]),(0,b.jsxs)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemCategory,s.k.docs.docSidebarItemCategoryLevel(r),"menu__list-item",{"menu__list-item--collapsed":g},p),children:[(0,b.jsxs)("div",{className:(0,o.Z)("menu__list-item-collapsible",{"menu__list-item-collapsible--active":v}),children:[(0,b.jsx)(A.Z,{className:(0,o.Z)("menu__link",{"menu__link--sublist":h,"menu__link--sublist-caret":!x&&h,"menu__link--active":_}),onClick:h?e=>{n?.(t),x?E(!1):(e.preventDefault(),E())}:()=>{n?.(t)},"aria-current":v?"page":void 0,"aria-expanded":h?!g:void 0,href:h?k??"#":k,...d,children:m}),x&&h&&(0,b.jsx)(y,{collapsed:g,categoryLabel:m,onClick:e=>{e.preventDefault(),E()}})]}),(0,b.jsx)(B.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:g,children:(0,b.jsx)(V,{items:u,tabIndex:g?-1:0,onItemClick:n,activePath:i,level:r+1})})]})}var E=n(71699),H=n(43399);const M={menuExternalLink:"menuExternalLink_NmtK"};function R(e){let{item:t,onItemClick:n,activePath:a,level:i,index:r,...c}=e;const{href:d,label:u,className:m,autoAddBaseUrl:h}=t,p=(0,l._F)(t,a),x=(0,E.Z)(d);return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(i),"menu__list-item",m),children:(0,b.jsxs)(A.Z,{className:(0,o.Z)("menu__link",!x&&M.menuExternalLink,{"menu__link--active":p}),autoAddBaseUrl:h,"aria-current":p?"page":void 0,to:d,...x&&{onClick:n?()=>n(t):void 0},...c,children:[u,!x&&(0,b.jsx)(H.Z,{})]})},u)}const W={menuHtmlItem:"menuHtmlItem_M9Kj"};function F(e){let{item:t,level:n,index:a}=e;const{value:i,defaultStyle:l,className:r}=t;return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(n),l&&[W.menuHtmlItem,"menu__list-item"],r),dangerouslySetInnerHTML:{__html:i}},a)}function P(e){let{item:t,...n}=e;switch(t.type){case"category":return(0,b.jsx)(w,{item:t,...n});case"html":return(0,b.jsx)(F,{item:t,...n});default:return(0,b.jsx)(R,{item:t,...n})}}function D(e){let{items:t,...n}=e;const a=(0,l.f)(t,n.activePath);return(0,b.jsx)(T,{children:a.map(((e,t)=>(0,b.jsx)(P,{item:e,index:t,...n},t)))})}const V=(0,a.memo)(D),U={menu:"menu_SIkG",menuWithAnnouncementBar:"menuWithAnnouncementBar_GW3s"};function K(e){let{path:t,sidebar:n,className:i}=e;const l=function(){const{isActive:e}=(0,C.nT)(),[t,n]=(0,a.useState)(e);return(0,d.RF)((t=>{let{scrollY:a}=t;e&&n(0===a)}),[e]),e&&t}();return(0,b.jsx)("nav",{"aria-label":(0,c.I)({id:"theme.docs.sidebar.navAriaLabel",message:"Docs sidebar",description:"The ARIA label for the sidebar navigation"}),className:(0,o.Z)("menu thin-scrollbar",U.menu,l&&U.menuWithAnnouncementBar,i),children:(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:n,activePath:t,level:1})})})}const Y="sidebar_njMd",z="sidebarWithHideableNavbar_wUlq",G="sidebarHidden_VK0M",O="sidebarLogo_isFc";function q(e){let{path:t,sidebar:n,onCollapse:a,isHidden:i}=e;const{navbar:{hideOnScroll:s},docs:{sidebar:{hideable:l}}}=(0,j.L)();return(0,b.jsxs)("div",{className:(0,o.Z)(Y,s&&z,i&&G),children:[s&&(0,b.jsx)(k.Z,{tabIndex:-1,className:O}),(0,b.jsx)(K,{path:t,sidebar:n}),l&&(0,b.jsx)(g,{onClick:a})]})}const J=a.memo(q);var Q=n(82306),X=n(35022);const $=e=>{let{sidebar:t,path:n}=e;const a=(0,X.e)();return(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:t,activePath:n,onItemClick:e=>{"category"===e.type&&e.href&&a.toggle(),"link"===e.type&&a.toggle()},level:1})})};function ee(e){return(0,b.jsx)(Q.Zo,{component:$,props:e})}const te=a.memo(ee);function ne(e){const t=(0,f.i)(),n="desktop"===t||"ssr"===t,a="mobile"===t;return(0,b.jsxs)(b.Fragment,{children:[n&&(0,b.jsx)(J,{...e}),a&&(0,b.jsx)(te,{...e})]})}const ae={expandButton:"expandButton_TmdG",expandButtonIcon:"expandButtonIcon_i1dp"};function oe(e){let{toggleSidebar:t}=e;return(0,b.jsx)("div",{className:ae.expandButton,title:(0,c.I)({id:"theme.docs.sidebar.expandButtonTitle",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.expandButtonAriaLabel",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),tabIndex:0,role:"button",onKeyDown:t,onClick:t,children:(0,b.jsx)(_,{className:ae.expandButtonIcon})})}const ie={docSidebarContainer:"docSidebarContainer_YfHR",docSidebarContainerHidden:"docSidebarContainerHidden_DPk8",sidebarViewport:"sidebarViewport_aRkj"};function se(e){let{children:t}=e;const n=(0,r.V)();return(0,b.jsx)(a.Fragment,{children:t},n?.name??"noSidebar")}function le(e){let{sidebar:t,hiddenSidebarContainer:n,setHiddenSidebarContainer:i}=e;const{pathname:l}=(0,x.TH)(),[r,c]=(0,a.useState)(!1),d=(0,a.useCallback)((()=>{r&&c(!1),!r&&(0,p.n)()&&c(!0),i((e=>!e))}),[i,r]);return(0,b.jsx)("aside",{className:(0,o.Z)(s.k.docs.docSidebarContainer,ie.docSidebarContainer,n&&ie.docSidebarContainerHidden),onTransitionEnd:e=>{e.currentTarget.classList.contains(ie.docSidebarContainer)&&n&&c(!0)},children:(0,b.jsx)(se,{children:(0,b.jsxs)("div",{className:(0,o.Z)(ie.sidebarViewport,r&&ie.sidebarViewportHidden),children:[(0,b.jsx)(ne,{sidebar:t,path:l,onCollapse:d,isHidden:r}),r&&(0,b.jsx)(oe,{toggleSidebar:d})]})})})}const re={docMainContainer:"docMainContainer_TBSr",docMainContainerEnhanced:"docMainContainerEnhanced_lQrH",docItemWrapperEnhanced:"docItemWrapperEnhanced_JWYK"};function ce(e){let{hiddenSidebarContainer:t,children:n}=e;const a=(0,r.V)();return(0,b.jsx)("main",{className:(0,o.Z)(re.docMainContainer,(t||!a)&&re.docMainContainerEnhanced),children:(0,b.jsx)("div",{className:(0,o.Z)("container padding-top--md padding-bottom--lg",re.docItemWrapper,t&&re.docItemWrapperEnhanced),children:n})})}const de={docRoot:"docRoot_UBD9",docsWrapper:"docsWrapper_hBAB"};function ue(e){let{children:t}=e;const n=(0,r.V)(),[o,i]=(0,a.useState)(!1);return(0,b.jsxs)("div",{className:de.docsWrapper,children:[(0,b.jsx)(h,{}),(0,b.jsxs)("div",{className:de.docRoot,children:[n&&(0,b.jsx)(le,{sidebar:n.items,hiddenSidebarContainer:o,setHiddenSidebarContainer:i}),(0,b.jsx)(ce,{hiddenSidebarContainer:o,children:t})]})]})}var me=n(89244);function be(e){const t=(0,l.SN)(e);if(!t)return(0,b.jsx)(me.Z,{});const{docElement:n,sidebarName:a,sidebarItems:c}=t;return(0,b.jsx)(i.FG,{className:(0,o.Z)(s.k.page.docsDocPage),children:(0,b.jsx)(r.b,{name:a,items:c,children:(0,b.jsx)(ue,{children:n})})})}},89244:(e,t,n)=>{n.d(t,{Z:()=>l});n(67294);var a=n(36905),o=n(11614),i=n(34055),s=n(85893);function l(e){let{className:t}=e;return(0,s.jsx)("main",{className:(0,a.Z)("container margin-vert--xl",t),children:(0,s.jsx)("div",{className:"row",children:(0,s.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,s.jsx)(i.Z,{as:"h1",className:"hero__title",children:(0,s.jsx)(o.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,s.jsx)("p",{children:(0,s.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,s.jsx)("p",{children:(0,s.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."})})]})})})}}}]); \ No newline at end of file diff --git a/assets/js/bfbfeea3.694435cb.js b/assets/js/bfbfeea3.a9c63372.js similarity index 99% rename from assets/js/bfbfeea3.694435cb.js rename to assets/js/bfbfeea3.a9c63372.js index 3481ff5d4..11b88495b 100644 --- a/assets/js/bfbfeea3.694435cb.js +++ b/assets/js/bfbfeea3.a9c63372.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2438],{50650:(l,c,s)=>{s.r(c),s.d(c,{default:()=>e});s(67294);var h=s(85893);function e(){return(0,h.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"108",height:"39",stroke:"#000",strokeLinecap:"round",strokeLinejoin:"round",fill:"#fff",fillRule:"evenodd",children:[(0,h.jsx)("defs",{children:(0,h.jsx)("clipPath",{id:"A",children:(0,h.jsx)("path",{d:"M15.7566 4.3862l-.0308.1504-.4803.1427-.6616.2276-.6558.2662c-.4359.1871-.868.3974-1.2904.6346l-1.2036.7561-.1003-.0463c-3.9985-1.5276-7.5494.3106-7.5494.3106-.3241 4.253 1.597 6.9341 1.977 7.4202l-.2642.7966c-.2951.9644-.517 1.9539-.6539 2.9762l-.054.4436C1.0956 20.2894 0 24.0294 0 24.0294c3.0842 3.5471 6.6795 3.767 6.6795 3.767s.0058-.0058.0097-.0077c.4571.8159.9856 1.5932 1.5816 2.3165l.7831.8795c-1.1245 3.2154.1582 5.8907.1582 5.8907 3.4333.1292 5.6881-1.5026 6.1626-1.8787l1.0377.3086c1.0551.272 2.1352.4321 3.2154.4784l.8081.0154h.1312l.0849-.0019.1697-.0058.1678-.0077.0039.0058c1.6164 2.3068 4.4614 2.6328 4.4614 2.6328 2.0233-2.1333 2.139-4.2492 2.139-4.7063h0v-.0309l-.0019-.0636-.0058-.0984c.4244-.2971.8294-.6172 1.2133-.9606a12.606 12.606 0 0 0 2.1043-2.4669l.162-.2566c2.2896.1312 3.904-1.4177 3.904-1.4177-.38-2.3859-1.7398-3.549-2.0233-3.7689h0s-.0116-.0096-.029-.0212l-.027-.0193-.0501-.0328.0347-.4301.0251-.7696-.002-.191-.0019-.0964v-.0483l-.0019-.0655-.0077-.1601-.0116-.2161-.0154-.2063-.0193-.1987-.0232-.1987-.027-.1967-.1485-.7793c-.2392-1.028-.6404-2.004-1.1708-2.8816s-1.1901-1.655-1.9327-2.3089-1.5758-1.1823-2.4496-1.5758-1.7919-.6462-2.7062-.7619c-.4571-.0598-.9123-.0829-1.3636-.0771l-.1678.0038-.0425.0019-.0579.002-.0694.0038-.1678.0116-.1852.0154-.6886.0965c-.9065.1697-1.7629.4976-2.5171.949s-1.41 1.0165-1.9442 1.6568-.9471 1.3463-1.2326 2.0793-.4455 1.4891-.4899 2.2239l-.0116.5478.0039.1351.0058.1465.0212.2624a5.907 5.907 0 0 0 .2045 1.0743c.1986.6886.5188 1.3116.9123 1.8401s.8641.9683 1.3637 1.3097 1.0338.5864 1.5623.7426a5.434 5.434 0 0 0 1.5528.2218l.1851-.0038.0984-.0039.0984-.0058.1581-.0154c.0116 0 .0289-.0039.0444-.0058l.0482-.0058.0964-.0135c.0656-.0077.1216-.0212.1794-.0309l.1736-.0385c.1138-.0251.2238-.0598.3337-.0926.216-.0714.4186-.1582.6076-.2546s.3626-.2064.5246-.3202l.135-.1022a.395.395 0 0 0 .0618-.5594c-.1216-.1485-.3299-.1871-.4957-.0945l-.1254.0656c-.1447.0694-.2951.135-.4552.1871s-.326.0926-.4996.1234l-.2623.0328c-.0444.0058-.0887.0077-.135.0077l-.1331.0039c-.0424 0-.0849-.0019-.1292-.0019l-.1621-.0078s-.027 0-.0058-.0019l-.0173-.0019-.0367-.0039-.0733-.0077-.1446-.0193c-.3877-.054-.7812-.1678-1.1592-.3395s-.7426-.4069-1.0705-.7021-.6134-.648-.8372-1.0492-.38-.8449-.4532-1.3116c-.0367-.2334-.0521-.4745-.0463-.7099l.0096-.1948c0 .0174.0019-.0096.0019-.0115l.002-.0232.0038-.0482.0097-.0964.0559-.382c.1794-1.0164.6886-2.0079 1.4756-2.762.1967-.1871.4089-.3627.6345-.517s.4668-.2932.7176-.4089.513-.2102.7812-.2835.5439-.1196.8236-.1447l.4204-.0173.0946.0019.1138.0038.0713.002c.029 0 0 0 .0135.0019l.029.0019.1138.0077c.3028.0251.6056.0676.9027.1351a6.44 6.44 0 0 1 1.7147.65c1.0802.5979 2.0002 1.5334 2.5653 2.6618a6.08 6.08 0 0 1 .5825 1.7919l.056.4725.0096.1196.0058.1196.0039.1196.0019.1119v.1022l-.0019.1157-.0135.2797-.0502.5169-.081.5092-.1138.5015c-.0829.3337-.1909.6597-.3163.9799-.2527.6403-.5902 1.2479-.9972 1.8092-.8159 1.1207-1.9288 2.0369-3.1942 2.6117-.6326.2854-1.3.4957-1.9867.6095-.3433.0578-.6905.0925-1.0377.1041l-.0636.002h-.056l-.1118.0019h-.1717-.0849c.0483 0-.0077 0-.0058-.0019l-.0347-.002-.5574-.027c-.7426-.0559-1.4775-.1871-2.1892-.3954s-1.4023-.4822-2.0562-.8294c-1.3058-.6982-2.4727-1.6549-3.387-2.8084-.4591-.5748-.8603-1.192-1.192-1.842s-.5922-1.3309-.7851-2.0272-.3105-1.41-.3568-2.1275l-.0077-.135-.0019-.0328v-.029l-.0019-.0597-.0039-.1177-.0019-.0289v-.0405-.083l-.002-.1678v-.0328c0 .0058 0 .0058 0-.0115v-.0656l.0058-.2623.0888-1.0841.1813-1.0955.2642-1.0763c.2025-.7079.4552-1.3946.7542-2.0426.5998-1.2982 1.3868-2.4419 2.3319-3.3639l.7349-.6539c.2546-.2045.5189-.3935.7908-.5709s.5497-.3414.8371-.4919l.436-.216.2218-.0984.2237-.0945c.299-.1273.6076-.2334.9162-.3337l.2334-.0714.2353-.0655.4745-.1216.2392-.052.2411-.0502.2411-.0443.1215-.0213.1215-.0192.2431-.0367.2739-.0366.2719-.0328.1717-.0174.1138-.0116.0578-.0058.0676-.0038.2758-.0174.137-.0096s.0501-.0019.0057-.0019l.029-.002.0578-.0019.2353-.0116h.9297c.6153.0251 1.2191.0926 1.8054.2006 1.1747.218 2.2799.596 3.279 1.0898s1.898 1.0917 2.6773 1.7456l.1446.1234.1408.1254.2759.2527.2661.2565.2546.2604.9124 1.0609c.5536.7117.9953 1.4292 1.3482 2.1082l.0656.1273.0617.1273.1196.2488.1138.245.1042.2392.3491.9065.3723 1.2383a.304.304 0 0 0 .3221.2276c.1543-.0135.2719-.1408.2758-.2951.0077-.3877-.0019-.8448-.0463-1.3656-.0579-.6442-.1678-1.3907-.3858-2.2104s-.5323-1.7167-.9952-2.6483-1.0705-1.898-1.8575-2.8335a14.502 14.502 0 0 0-1.0049-1.0821c.54-2.1487-.6578-4.0119-.6578-4.0119-2.0677-.1293-3.3831.6423-3.8711.9952l-.2469-.1041-1.086-.3935-1.1399-.3163-1.1881-.2295-.2122-.0309C21.7862 1.2055 19.1957 0 19.1957 0c-2.8894 1.8227-3.4391 4.3862-3.4391 4.3862",stroke:"none",strokeLinejoin:"miter",strokeLinecap:"butt",fill:"#000",fillRule:"nonzero",strokeWidth:".0193",transform:"translate(0 .0077)"})})}),(0,h.jsxs)("g",{fill:"#000",stroke:"none",fillRule:"nonzero",children:[(0,h.jsx)("path",{d:"M55.4146 19.4587c-.1404 3.6146-2.9917 6.4302-6.5348 6.4302-3.7396 0-6.5195-3.0275-6.5195-6.6778 0-3.686 3.0096-6.7135 6.6778-6.7135 1.6567 0 3.2776.7122 4.6484 1.9936l-1.0696 1.3172c-1.0517-.9087-2.3153-1.5316-3.5788-1.5316-2.6905 0-4.8986 2.2081-4.8986 4.9343 0 2.7594 2.083 4.8986 4.7378 4.8986 2.3867 0 4.2578-1.7461 4.6305-3.9898h-5.4117v-1.5853h7.3211v.9241zm6.2157-.873h-.9981c-1.1028 0-1.9936.8935-1.9936 1.9937v5.1997h-1.7818v-8.9062h1.4601v.7479c.4799-.4799 1.2457-.7479 2.1009-.7479h1.9221zm9.7053 7.1959h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM69.513 21.968c.4211-1.8099-.9497-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.09-.1864 1.986-1.0339 2.2362-2.1136m5.4116-5.5903v.4978h2.8309v1.5673h-2.8309v7.3389h-1.7639v-9.3504c0-1.9604 1.4065-3.1168 3.1704-3.1168h2.1366l-.7122 1.6746h-1.4218c-.7837 0-1.4091.6228-1.4091 1.3886m12.2707 9.4039h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zm-1.8201-3.8136c.4212-1.8099-.9496-3.4538-2.7364-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.5239 3.0734 3.2955 2.7722 1.0874-.1864 1.986-1.0339 2.2361-2.1136m11.3338-1.4602v5.2713h-1.7817v-5.2713c0-1.1053-.9087-1.9936-1.9936-1.9936-1.1232 0-2.0115.8909-2.0115 1.9936v5.2713h-1.7818v-8.9062h1.478v.7658c.6407-.5693 1.4959-.9088 2.3868-.9088 2.0651.0026 3.7038 1.695 3.7038 3.7779m10.4736 5.2738h-1.5138v-1.1231c-1.1716 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM105.36 21.968c.4211-1.8099-.9496-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.0875-.1864 1.986-1.0339 2.2362-2.1136"}),(0,h.jsx)("path",{d:"M0 0h35.3825v38.4281H0z",transform:"translate(0 -.0077)",clipPath:"url(#A)"})]})]})}}}]); \ No newline at end of file +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2438],{97322:(l,c,s)=>{s.r(c),s.d(c,{default:()=>e});s(67294);var h=s(85893);function e(){return(0,h.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"108",height:"39",stroke:"#000",strokeLinecap:"round",strokeLinejoin:"round",fill:"#fff",fillRule:"evenodd",children:[(0,h.jsx)("defs",{children:(0,h.jsx)("clipPath",{id:"A",children:(0,h.jsx)("path",{d:"M15.7566 4.3862l-.0308.1504-.4803.1427-.6616.2276-.6558.2662c-.4359.1871-.868.3974-1.2904.6346l-1.2036.7561-.1003-.0463c-3.9985-1.5276-7.5494.3106-7.5494.3106-.3241 4.253 1.597 6.9341 1.977 7.4202l-.2642.7966c-.2951.9644-.517 1.9539-.6539 2.9762l-.054.4436C1.0956 20.2894 0 24.0294 0 24.0294c3.0842 3.5471 6.6795 3.767 6.6795 3.767s.0058-.0058.0097-.0077c.4571.8159.9856 1.5932 1.5816 2.3165l.7831.8795c-1.1245 3.2154.1582 5.8907.1582 5.8907 3.4333.1292 5.6881-1.5026 6.1626-1.8787l1.0377.3086c1.0551.272 2.1352.4321 3.2154.4784l.8081.0154h.1312l.0849-.0019.1697-.0058.1678-.0077.0039.0058c1.6164 2.3068 4.4614 2.6328 4.4614 2.6328 2.0233-2.1333 2.139-4.2492 2.139-4.7063h0v-.0309l-.0019-.0636-.0058-.0984c.4244-.2971.8294-.6172 1.2133-.9606a12.606 12.606 0 0 0 2.1043-2.4669l.162-.2566c2.2896.1312 3.904-1.4177 3.904-1.4177-.38-2.3859-1.7398-3.549-2.0233-3.7689h0s-.0116-.0096-.029-.0212l-.027-.0193-.0501-.0328.0347-.4301.0251-.7696-.002-.191-.0019-.0964v-.0483l-.0019-.0655-.0077-.1601-.0116-.2161-.0154-.2063-.0193-.1987-.0232-.1987-.027-.1967-.1485-.7793c-.2392-1.028-.6404-2.004-1.1708-2.8816s-1.1901-1.655-1.9327-2.3089-1.5758-1.1823-2.4496-1.5758-1.7919-.6462-2.7062-.7619c-.4571-.0598-.9123-.0829-1.3636-.0771l-.1678.0038-.0425.0019-.0579.002-.0694.0038-.1678.0116-.1852.0154-.6886.0965c-.9065.1697-1.7629.4976-2.5171.949s-1.41 1.0165-1.9442 1.6568-.9471 1.3463-1.2326 2.0793-.4455 1.4891-.4899 2.2239l-.0116.5478.0039.1351.0058.1465.0212.2624a5.907 5.907 0 0 0 .2045 1.0743c.1986.6886.5188 1.3116.9123 1.8401s.8641.9683 1.3637 1.3097 1.0338.5864 1.5623.7426a5.434 5.434 0 0 0 1.5528.2218l.1851-.0038.0984-.0039.0984-.0058.1581-.0154c.0116 0 .0289-.0039.0444-.0058l.0482-.0058.0964-.0135c.0656-.0077.1216-.0212.1794-.0309l.1736-.0385c.1138-.0251.2238-.0598.3337-.0926.216-.0714.4186-.1582.6076-.2546s.3626-.2064.5246-.3202l.135-.1022a.395.395 0 0 0 .0618-.5594c-.1216-.1485-.3299-.1871-.4957-.0945l-.1254.0656c-.1447.0694-.2951.135-.4552.1871s-.326.0926-.4996.1234l-.2623.0328c-.0444.0058-.0887.0077-.135.0077l-.1331.0039c-.0424 0-.0849-.0019-.1292-.0019l-.1621-.0078s-.027 0-.0058-.0019l-.0173-.0019-.0367-.0039-.0733-.0077-.1446-.0193c-.3877-.054-.7812-.1678-1.1592-.3395s-.7426-.4069-1.0705-.7021-.6134-.648-.8372-1.0492-.38-.8449-.4532-1.3116c-.0367-.2334-.0521-.4745-.0463-.7099l.0096-.1948c0 .0174.0019-.0096.0019-.0115l.002-.0232.0038-.0482.0097-.0964.0559-.382c.1794-1.0164.6886-2.0079 1.4756-2.762.1967-.1871.4089-.3627.6345-.517s.4668-.2932.7176-.4089.513-.2102.7812-.2835.5439-.1196.8236-.1447l.4204-.0173.0946.0019.1138.0038.0713.002c.029 0 0 0 .0135.0019l.029.0019.1138.0077c.3028.0251.6056.0676.9027.1351a6.44 6.44 0 0 1 1.7147.65c1.0802.5979 2.0002 1.5334 2.5653 2.6618a6.08 6.08 0 0 1 .5825 1.7919l.056.4725.0096.1196.0058.1196.0039.1196.0019.1119v.1022l-.0019.1157-.0135.2797-.0502.5169-.081.5092-.1138.5015c-.0829.3337-.1909.6597-.3163.9799-.2527.6403-.5902 1.2479-.9972 1.8092-.8159 1.1207-1.9288 2.0369-3.1942 2.6117-.6326.2854-1.3.4957-1.9867.6095-.3433.0578-.6905.0925-1.0377.1041l-.0636.002h-.056l-.1118.0019h-.1717-.0849c.0483 0-.0077 0-.0058-.0019l-.0347-.002-.5574-.027c-.7426-.0559-1.4775-.1871-2.1892-.3954s-1.4023-.4822-2.0562-.8294c-1.3058-.6982-2.4727-1.6549-3.387-2.8084-.4591-.5748-.8603-1.192-1.192-1.842s-.5922-1.3309-.7851-2.0272-.3105-1.41-.3568-2.1275l-.0077-.135-.0019-.0328v-.029l-.0019-.0597-.0039-.1177-.0019-.0289v-.0405-.083l-.002-.1678v-.0328c0 .0058 0 .0058 0-.0115v-.0656l.0058-.2623.0888-1.0841.1813-1.0955.2642-1.0763c.2025-.7079.4552-1.3946.7542-2.0426.5998-1.2982 1.3868-2.4419 2.3319-3.3639l.7349-.6539c.2546-.2045.5189-.3935.7908-.5709s.5497-.3414.8371-.4919l.436-.216.2218-.0984.2237-.0945c.299-.1273.6076-.2334.9162-.3337l.2334-.0714.2353-.0655.4745-.1216.2392-.052.2411-.0502.2411-.0443.1215-.0213.1215-.0192.2431-.0367.2739-.0366.2719-.0328.1717-.0174.1138-.0116.0578-.0058.0676-.0038.2758-.0174.137-.0096s.0501-.0019.0057-.0019l.029-.002.0578-.0019.2353-.0116h.9297c.6153.0251 1.2191.0926 1.8054.2006 1.1747.218 2.2799.596 3.279 1.0898s1.898 1.0917 2.6773 1.7456l.1446.1234.1408.1254.2759.2527.2661.2565.2546.2604.9124 1.0609c.5536.7117.9953 1.4292 1.3482 2.1082l.0656.1273.0617.1273.1196.2488.1138.245.1042.2392.3491.9065.3723 1.2383a.304.304 0 0 0 .3221.2276c.1543-.0135.2719-.1408.2758-.2951.0077-.3877-.0019-.8448-.0463-1.3656-.0579-.6442-.1678-1.3907-.3858-2.2104s-.5323-1.7167-.9952-2.6483-1.0705-1.898-1.8575-2.8335a14.502 14.502 0 0 0-1.0049-1.0821c.54-2.1487-.6578-4.0119-.6578-4.0119-2.0677-.1293-3.3831.6423-3.8711.9952l-.2469-.1041-1.086-.3935-1.1399-.3163-1.1881-.2295-.2122-.0309C21.7862 1.2055 19.1957 0 19.1957 0c-2.8894 1.8227-3.4391 4.3862-3.4391 4.3862",stroke:"none",strokeLinejoin:"miter",strokeLinecap:"butt",fill:"#000",fillRule:"nonzero",strokeWidth:".0193",transform:"translate(0 .0077)"})})}),(0,h.jsxs)("g",{fill:"#000",stroke:"none",fillRule:"nonzero",children:[(0,h.jsx)("path",{d:"M55.4146 19.4587c-.1404 3.6146-2.9917 6.4302-6.5348 6.4302-3.7396 0-6.5195-3.0275-6.5195-6.6778 0-3.686 3.0096-6.7135 6.6778-6.7135 1.6567 0 3.2776.7122 4.6484 1.9936l-1.0696 1.3172c-1.0517-.9087-2.3153-1.5316-3.5788-1.5316-2.6905 0-4.8986 2.2081-4.8986 4.9343 0 2.7594 2.083 4.8986 4.7378 4.8986 2.3867 0 4.2578-1.7461 4.6305-3.9898h-5.4117v-1.5853h7.3211v.9241zm6.2157-.873h-.9981c-1.1028 0-1.9936.8935-1.9936 1.9937v5.1997h-1.7818v-8.9062h1.4601v.7479c.4799-.4799 1.2457-.7479 2.1009-.7479h1.9221zm9.7053 7.1959h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM69.513 21.968c.4211-1.8099-.9497-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.09-.1864 1.986-1.0339 2.2362-2.1136m5.4116-5.5903v.4978h2.8309v1.5673h-2.8309v7.3389h-1.7639v-9.3504c0-1.9604 1.4065-3.1168 3.1704-3.1168h2.1366l-.7122 1.6746h-1.4218c-.7837 0-1.4091.6228-1.4091 1.3886m12.2707 9.4039h-1.5138v-1.1231c-1.1717 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zm-1.8201-3.8136c.4212-1.8099-.9496-3.4538-2.7364-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.5239 3.0734 3.2955 2.7722 1.0874-.1864 1.986-1.0339 2.2361-2.1136m11.3338-1.4602v5.2713h-1.7817v-5.2713c0-1.1053-.9087-1.9936-1.9936-1.9936-1.1232 0-2.0115.8909-2.0115 1.9936v5.2713h-1.7818v-8.9062h1.478v.7658c.6407-.5693 1.4959-.9088 2.3868-.9088 2.0651.0026 3.7038 1.695 3.7038 3.7779m10.4736 5.2738h-1.5138v-1.1231c-1.1716 1.1436-3.0172 1.6924-4.8883.8704-1.3861-.6101-2.4174-1.8736-2.6931-3.3644-.5309-2.8794 1.6848-5.4346 4.5157-5.4346 1.1921 0 2.2616.4799 3.0453 1.2636v-1.1232h1.5316v8.9113zM105.36 21.968c.4211-1.8099-.9496-3.4538-2.7365-3.4538-1.5495 0-2.7952 1.2636-2.7952 2.7952 0 1.7332 1.524 3.0734 3.2955 2.7722 1.0875-.1864 1.986-1.0339 2.2362-2.1136"}),(0,h.jsx)("path",{d:"M0 0h35.3825v38.4281H0z",transform:"translate(0 -.0077)",clipPath:"url(#A)"})]})]})}}}]); \ No newline at end of file diff --git a/assets/js/ccc49370.76c9d1ca.js b/assets/js/ccc49370.76c9d1ca.js deleted file mode 100644 index 02fb76610..000000000 --- a/assets/js/ccc49370.76c9d1ca.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6103],{61460:(e,t,a)=>{a.d(t,{Z:()=>v});var s=a(67294),n=a(36905),r=a(7372),i=a(87524),l=a(39960),o=a(95999),c=a(16550),m=a(48596);function d(e){const{pathname:t}=(0,c.TH)();return(0,s.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,m.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 h=a(85893);function g(e){let{sidebar:t}=e;const a=d(t.items);return(0,h.jsx)("aside",{className:"col col--3",children:(0,h.jsxs)("nav",{className:(0,n.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,h.jsx)("div",{className:(0,n.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,h.jsx)("ul",{className:(0,n.Z)(u.sidebarItemList,"clean-list"),children:a.map((e=>(0,h.jsx)("li",{className:u.sidebarItem,children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var p=a(13102);function x(e){let{sidebar:t}=e;const a=d(t.items);return(0,h.jsx)("ul",{className:"menu__list",children:a.map((e=>(0,h.jsx)("li",{className:"menu__list-item",children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function j(e){return(0,h.jsx)(p.Zo,{component:x,props:e})}function b(e){let{sidebar:t}=e;const a=(0,i.i)();return t?.items.length?"mobile"===a?(0,h.jsx)(j,{sidebar:t}):(0,h.jsx)(g,{sidebar:t}):null}function v(e){const{sidebar:t,toc:a,children:s,...i}=e,l=t&&t.items.length>0;return(0,h.jsx)(r.Z,{...i,children:(0,h.jsx)("div",{className:"container margin-vert--lg",children:(0,h.jsxs)("div",{className:"row",children:[(0,h.jsx)(b,{sidebar:t}),(0,h.jsx)("main",{className:(0,n.Z)("col",{"col--7":l,"col--9 col--offset-1":!l}),itemScope:!0,itemType:"https://schema.org/Blog",children:s}),a&&(0,h.jsx)("div",{className:"col col--2",children:a})]})})})}},15289:(e,t,a)=>{a.d(t,{Z:()=>i});a(67294);var s=a(44996),n=a(9460),r=a(85893);function i(e){let{children:t,className:a}=e;const{frontMatter:i,assets:l,metadata:{description:o}}=(0,n.C)(),{withBaseUrl:c}=(0,s.C)(),m=l.image??i.image,d=i.keywords??[];return(0,r.jsxs)("article",{className:a,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,r.jsx)("meta",{itemProp:"description",content:o}),m&&(0,r.jsx)("link",{itemProp:"image",href:c(m,{absolute:!0})}),d.length>0&&(0,r.jsx)("meta",{itemProp:"keywords",content:d.join(",")}),t]})}},79419:(e,t,a)=>{a.r(t),a.d(t,{default:()=>H});a(67294);var s=a(36905),n=a(10833),r=a(35281),i=a(9460),l=a(61460),o=a(15289),c=a(39960);const m={title:"title_f1Hy"};var d=a(85893);function u(e){let{className:t}=e;const{metadata:a,isBlogPostPage:n}=(0,i.C)(),{permalink:r,title:l}=a,o=n?"h1":"h2";return(0,d.jsx)(o,{className:(0,s.Z)(m.title,t),itemProp:"headline",children:n?l:(0,d.jsx)(c.Z,{itemProp:"url",to:r,children:l})})}var h=a(95999),g=a(88824);const p={container:"container_mt6G"};function x(e){let{readingTime:t}=e;const a=function(){const{selectMessage:e}=(0,g.c)();return t=>{const a=Math.ceil(t);return e(a,(0,h.I)({id:"theme.blog.post.readingTime.plurals",description:'Pluralized label for "{readingTime} min read". 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 min read|{readingTime} min read"},{readingTime:a}))}}();return(0,d.jsx)(d.Fragment,{children:a(t)})}function j(e){let{date:t,formattedDate:a}=e;return(0,d.jsx)("time",{dateTime:t,itemProp:"datePublished",children:a})}function b(){return(0,d.jsx)(d.Fragment,{children:" \xb7 "})}function v(e){let{className:t}=e;const{metadata:a}=(0,i.C)(),{date:n,formattedDate:r,readingTime:l}=a;return(0,d.jsxs)("div",{className:(0,s.Z)(p.container,"margin-vert--md",t),children:[(0,d.jsx)(j,{date:n,formattedDate:r}),void 0!==l&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(b,{}),(0,d.jsx)(x,{readingTime:l})]})]})}function f(e){return e.href?(0,d.jsx)(c.Z,{...e}):(0,d.jsx)(d.Fragment,{children:e.children})}function _(e){let{author:t,className:a}=e;const{name:n,title:r,url:i,imageURL:l,email:o}=t,c=i||o&&`mailto:${o}`||void 0;return(0,d.jsxs)("div",{className:(0,s.Z)("avatar margin-bottom--sm",a),children:[l&&(0,d.jsx)(f,{href:c,className:"avatar__photo-link",children:(0,d.jsx)("img",{className:"avatar__photo",src:l,alt:n,itemProp:"image"})}),n&&(0,d.jsxs)("div",{className:"avatar__intro",itemProp:"author",itemScope:!0,itemType:"https://schema.org/Person",children:[(0,d.jsx)("div",{className:"avatar__name",children:(0,d.jsx)(f,{href:c,itemProp:"url",children:(0,d.jsx)("span",{itemProp:"name",children:n})})}),r&&(0,d.jsx)("small",{className:"avatar__subtitle",itemProp:"description",children:r})]})]})}const P={authorCol:"authorCol_Hf19",imageOnlyAuthorRow:"imageOnlyAuthorRow_pa_O",imageOnlyAuthorCol:"imageOnlyAuthorCol_G86a"};function N(e){let{className:t}=e;const{metadata:{authors:a},assets:n}=(0,i.C)();if(0===a.length)return null;const r=a.every((e=>{let{name:t}=e;return!t}));return(0,d.jsx)("div",{className:(0,s.Z)("margin-top--md margin-bottom--sm",r?P.imageOnlyAuthorRow:"row",t),children:a.map(((e,t)=>(0,d.jsx)("div",{className:(0,s.Z)(!r&&"col col--6",r?P.imageOnlyAuthorCol:P.authorCol),children:(0,d.jsx)(_,{author:{...e,imageURL:n.authorsImageUrls[t]??e.imageURL}})},t)))})}function Z(){return(0,d.jsxs)("header",{children:[(0,d.jsx)(u,{}),(0,d.jsx)(v,{}),(0,d.jsx)(N,{})]})}var k=a(18780),I=a(40591);function w(e){let{children:t,className:a}=e;const{isBlogPostPage:n}=(0,i.C)();return(0,d.jsx)("div",{id:n?k.blogPostContainerID:void 0,className:(0,s.Z)("markdown",a),itemProp:"articleBody",children:(0,d.jsx)(I.Z,{children:t})})}var C=a(84881),T=a(71526);function y(){return(0,d.jsx)("b",{children:(0,d.jsx)(h.Z,{id:"theme.blog.post.readMore",description:"The label used in blog post item excerpts to link to full blog posts",children:"Read More"})})}function L(e){const{blogPostTitle:t,...a}=e;return(0,d.jsx)(c.Z,{"aria-label":(0,h.I)({message:"Read more about {title}",id:"theme.blog.post.readMoreLabel",description:"The ARIA label for the link to full blog posts from excerpts"},{title:t}),...a,children:(0,d.jsx)(y,{})})}const F={blogPostFooterDetailsFull:"blogPostFooterDetailsFull_mRVl"};function B(){const{metadata:e,isBlogPostPage:t}=(0,i.C)(),{tags:a,title:n,editUrl:r,hasTruncateMarker:l}=e,o=!t&&l,c=a.length>0;return c||o||r?(0,d.jsxs)("footer",{className:(0,s.Z)("row docusaurus-mt-lg",t&&F.blogPostFooterDetailsFull),children:[c&&(0,d.jsx)("div",{className:(0,s.Z)("col",{"col--9":o}),children:(0,d.jsx)(T.Z,{tags:a})}),t&&r&&(0,d.jsx)("div",{className:"col margin-top--sm",children:(0,d.jsx)(C.Z,{editUrl:r})}),o&&(0,d.jsx)("div",{className:(0,s.Z)("col text--right",{"col--3":c}),children:(0,d.jsx)(L,{blogPostTitle:n,to:e.permalink})})]}):null}function A(e){let{children:t,className:a}=e;const n=function(){const{isBlogPostPage:e}=(0,i.C)();return e?void 0:"margin-bottom--xl"}();return(0,d.jsxs)(o.Z,{className:(0,s.Z)(n,a),children:[(0,d.jsx)(Z,{}),(0,d.jsx)(w,{children:t}),(0,d.jsx)(B,{})]})}var M=a(32244);function R(e){const{nextItem:t,prevItem:a}=e;return(0,d.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,h.I)({id:"theme.blog.post.paginator.navAriaLabel",message:"Blog post page navigation",description:"The ARIA label for the blog posts pagination"}),children:[a&&(0,d.jsx)(M.Z,{...a,subLabel:(0,d.jsx)(h.Z,{id:"theme.blog.post.paginator.newerPost",description:"The blog post button label to navigate to the newer/previous post",children:"Newer Post"})}),t&&(0,d.jsx)(M.Z,{...t,subLabel:(0,d.jsx)(h.Z,{id:"theme.blog.post.paginator.olderPost",description:"The blog post button label to navigate to the older/next post",children:"Older Post"}),isNext:!0})]})}function O(){const{assets:e,metadata:t}=(0,i.C)(),{title:a,description:s,date:r,tags:l,authors:o,frontMatter:c}=t,{keywords:m}=c,u=e.image??c.image;return(0,d.jsxs)(n.d,{title:a,description:s,keywords:m,image:u,children:[(0,d.jsx)("meta",{property:"og:type",content:"article"}),(0,d.jsx)("meta",{property:"article:published_time",content:r}),o.some((e=>e.url))&&(0,d.jsx)("meta",{property:"article:author",content:o.map((e=>e.url)).filter(Boolean).join(",")}),l.length>0&&(0,d.jsx)("meta",{property:"article:tag",content:l.map((e=>e.label)).join(",")})]})}var D=a(39407),U=a(22212);function $(e){let{sidebar:t,children:a}=e;const{metadata:s,toc:n}=(0,i.C)(),{nextItem:r,prevItem:o,frontMatter:c,unlisted:m}=s,{hide_table_of_contents:u,toc_min_heading_level:h,toc_max_heading_level:g}=c;return(0,d.jsxs)(l.Z,{sidebar:t,toc:!u&&n.length>0?(0,d.jsx)(D.Z,{toc:n,minHeadingLevel:h,maxHeadingLevel:g}):void 0,children:[m&&(0,d.jsx)(U.Z,{}),(0,d.jsx)(A,{children:a}),(r||o)&&(0,d.jsx)(R,{nextItem:r,prevItem:o})]})}function H(e){const t=e.content;return(0,d.jsx)(i.n,{content:e.content,isBlogPostPage:!0,children:(0,d.jsxs)(n.FG,{className:(0,s.Z)(r.k.wrapper.blogPages,r.k.page.blogPostPage),children:[(0,d.jsx)(O,{}),(0,d.jsx)($,{sidebar:e.sidebar,children:(0,d.jsx)(t,{})})]})})}},84881:(e,t,a)=>{a.d(t,{Z:()=>m});a(67294);var s=a(95999),n=a(35281),r=a(39960),i=a(36905);const l={iconEdit:"iconEdit_Z9Sw"};var o=a(85893);function c(e){let{className:t,...a}=e;return(0,o.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,i.Z)(l.iconEdit,t),"aria-hidden":"true",...a,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 m(e){let{editUrl:t}=e;return(0,o.jsxs)(r.Z,{to:t,className:n.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"})]})}},32244:(e,t,a)=>{a.d(t,{Z:()=>i});a(67294);var s=a(36905),n=a(39960),r=a(85893);function i(e){const{permalink:t,title:a,subLabel:i,isNext:l}=e;return(0,r.jsxs)(n.Z,{className:(0,s.Z)("pagination-nav__link",l?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[i&&(0,r.jsx)("div",{className:"pagination-nav__sublabel",children:i}),(0,r.jsx)("div",{className:"pagination-nav__label",children:a})]})}},13008:(e,t,a)=>{a.d(t,{Z:()=>l});a(67294);var s=a(36905),n=a(39960);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var i=a(85893);function l(e){let{permalink:t,label:a,count:l}=e;return(0,i.jsxs)(n.Z,{href:t,className:(0,s.Z)(r.tag,l?r.tagWithCount:r.tagRegular),children:[a,l&&(0,i.jsx)("span",{children:l})]})}},71526:(e,t,a)=>{a.d(t,{Z:()=>o});a(67294);var s=a(36905),n=a(95999),r=a(13008);const i={tags:"tags_jXut",tag:"tag_QGVx"};var l=a(85893);function o(e){let{tags:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("b",{children:(0,l.jsx)(n.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,l.jsx)("ul",{className:(0,s.Z)(i.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:a}=e;return(0,l.jsx)("li",{className:i.tag,children:(0,l.jsx)(r.Z,{label:t,permalink:a})},a)}))})]})}},9460:(e,t,a)=>{a.d(t,{C:()=>o,n:()=>l});var s=a(67294),n=a(902),r=a(85893);const i=s.createContext(null);function l(e){let{children:t,content:a,isBlogPostPage:n=!1}=e;const l=function(e){let{content:t,isBlogPostPage:a}=e;return(0,s.useMemo)((()=>({metadata:t.metadata,frontMatter:t.frontMatter,assets:t.assets,toc:t.toc,isBlogPostPage:a})),[t,a])}({content:a,isBlogPostPage:n});return(0,r.jsx)(i.Provider,{value:l,children:t})}function o(){const e=(0,s.useContext)(i);if(null===e)throw new n.i6("BlogPostProvider");return e}},88824:(e,t,a)=>{a.d(t,{c:()=>c});var s=a(67294),n=a(52263);const r=["zero","one","two","few","many","other"];function i(e){return r.filter((t=>e.includes(t)))}const l={locale:"en",pluralForms:i(["one","other"]),select:e=>1===e?"one":"other"};function o(){const{i18n:{currentLocale:e}}=(0,n.Z)();return(0,s.useMemo)((()=>{try{return function(e){const t=new Intl.PluralRules(e);return{locale:e,pluralForms:i(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`),l}}),[e])}function c(){const e=o();return{selectMessage:(t,a)=>function(e,t,a){const s=e.split("|");if(1===s.length)return s[0];s.length>a.pluralForms.length&&console.error(`For locale=${a.locale}, a maximum of ${a.pluralForms.length} plural forms are expected (${a.pluralForms.join(",")}), but the message contains ${s.length}: ${e}`);const n=a.select(t),r=a.pluralForms.indexOf(n);return s[Math.min(r,s.length-1)]}(a,t,e)}}}}]); \ No newline at end of file diff --git a/assets/js/ccc49370.c475783f.js b/assets/js/ccc49370.c475783f.js new file mode 100644 index 000000000..8d92cf890 --- /dev/null +++ b/assets/js/ccc49370.c475783f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[6103],{38762:(e,t,a)=>{a.d(t,{Z:()=>v});var s=a(67294),n=a(36905),r=a(78299),i=a(94980),l=a(75013),o=a(11614),c=a(16550),m=a(18407);function d(e){const{pathname:t}=(0,c.TH)();return(0,s.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,m.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 h=a(85893);function g(e){let{sidebar:t}=e;const a=d(t.items);return(0,h.jsx)("aside",{className:"col col--3",children:(0,h.jsxs)("nav",{className:(0,n.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,h.jsx)("div",{className:(0,n.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,h.jsx)("ul",{className:(0,n.Z)(u.sidebarItemList,"clean-list"),children:a.map((e=>(0,h.jsx)("li",{className:u.sidebarItem,children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var p=a(82306);function x(e){let{sidebar:t}=e;const a=d(t.items);return(0,h.jsx)("ul",{className:"menu__list",children:a.map((e=>(0,h.jsx)("li",{className:"menu__list-item",children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function j(e){return(0,h.jsx)(p.Zo,{component:x,props:e})}function b(e){let{sidebar:t}=e;const a=(0,i.i)();return t?.items.length?"mobile"===a?(0,h.jsx)(j,{sidebar:t}):(0,h.jsx)(g,{sidebar:t}):null}function v(e){const{sidebar:t,toc:a,children:s,...i}=e,l=t&&t.items.length>0;return(0,h.jsx)(r.Z,{...i,children:(0,h.jsx)("div",{className:"container margin-vert--lg",children:(0,h.jsxs)("div",{className:"row",children:[(0,h.jsx)(b,{sidebar:t}),(0,h.jsx)("main",{className:(0,n.Z)("col",{"col--7":l,"col--9 col--offset-1":!l}),itemScope:!0,itemType:"https://schema.org/Blog",children:s}),a&&(0,h.jsx)("div",{className:"col col--2",children:a})]})})})}},83400:(e,t,a)=>{a.d(t,{Z:()=>i});a(67294);var s=a(51402),n=a(17762),r=a(85893);function i(e){let{children:t,className:a}=e;const{frontMatter:i,assets:l,metadata:{description:o}}=(0,n.C)(),{withBaseUrl:c}=(0,s.C)(),m=l.image??i.image,d=i.keywords??[];return(0,r.jsxs)("article",{className:a,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,r.jsx)("meta",{itemProp:"description",content:o}),m&&(0,r.jsx)("link",{itemProp:"image",href:c(m,{absolute:!0})}),d.length>0&&(0,r.jsx)("meta",{itemProp:"keywords",content:d.join(",")}),t]})}},334:(e,t,a)=>{a.r(t),a.d(t,{default:()=>H});a(67294);var s=a(36905),n=a(44873),r=a(18015),i=a(17762),l=a(38762),o=a(83400),c=a(75013);const m={title:"title_f1Hy"};var d=a(85893);function u(e){let{className:t}=e;const{metadata:a,isBlogPostPage:n}=(0,i.C)(),{permalink:r,title:l}=a,o=n?"h1":"h2";return(0,d.jsx)(o,{className:(0,s.Z)(m.title,t),itemProp:"headline",children:n?l:(0,d.jsx)(c.Z,{itemProp:"url",to:r,children:l})})}var h=a(11614),g=a(57880);const p={container:"container_mt6G"};function x(e){let{readingTime:t}=e;const a=function(){const{selectMessage:e}=(0,g.c)();return t=>{const a=Math.ceil(t);return e(a,(0,h.I)({id:"theme.blog.post.readingTime.plurals",description:'Pluralized label for "{readingTime} min read". 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 min read|{readingTime} min read"},{readingTime:a}))}}();return(0,d.jsx)(d.Fragment,{children:a(t)})}function j(e){let{date:t,formattedDate:a}=e;return(0,d.jsx)("time",{dateTime:t,itemProp:"datePublished",children:a})}function b(){return(0,d.jsx)(d.Fragment,{children:" \xb7 "})}function v(e){let{className:t}=e;const{metadata:a}=(0,i.C)(),{date:n,formattedDate:r,readingTime:l}=a;return(0,d.jsxs)("div",{className:(0,s.Z)(p.container,"margin-vert--md",t),children:[(0,d.jsx)(j,{date:n,formattedDate:r}),void 0!==l&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(b,{}),(0,d.jsx)(x,{readingTime:l})]})]})}function f(e){return e.href?(0,d.jsx)(c.Z,{...e}):(0,d.jsx)(d.Fragment,{children:e.children})}function _(e){let{author:t,className:a}=e;const{name:n,title:r,url:i,imageURL:l,email:o}=t,c=i||o&&`mailto:${o}`||void 0;return(0,d.jsxs)("div",{className:(0,s.Z)("avatar margin-bottom--sm",a),children:[l&&(0,d.jsx)(f,{href:c,className:"avatar__photo-link",children:(0,d.jsx)("img",{className:"avatar__photo",src:l,alt:n,itemProp:"image"})}),n&&(0,d.jsxs)("div",{className:"avatar__intro",itemProp:"author",itemScope:!0,itemType:"https://schema.org/Person",children:[(0,d.jsx)("div",{className:"avatar__name",children:(0,d.jsx)(f,{href:c,itemProp:"url",children:(0,d.jsx)("span",{itemProp:"name",children:n})})}),r&&(0,d.jsx)("small",{className:"avatar__subtitle",itemProp:"description",children:r})]})]})}const P={authorCol:"authorCol_Hf19",imageOnlyAuthorRow:"imageOnlyAuthorRow_pa_O",imageOnlyAuthorCol:"imageOnlyAuthorCol_G86a"};function N(e){let{className:t}=e;const{metadata:{authors:a},assets:n}=(0,i.C)();if(0===a.length)return null;const r=a.every((e=>{let{name:t}=e;return!t}));return(0,d.jsx)("div",{className:(0,s.Z)("margin-top--md margin-bottom--sm",r?P.imageOnlyAuthorRow:"row",t),children:a.map(((e,t)=>(0,d.jsx)("div",{className:(0,s.Z)(!r&&"col col--6",r?P.imageOnlyAuthorCol:P.authorCol),children:(0,d.jsx)(_,{author:{...e,imageURL:n.authorsImageUrls[t]??e.imageURL}})},t)))})}function Z(){return(0,d.jsxs)("header",{children:[(0,d.jsx)(u,{}),(0,d.jsx)(v,{}),(0,d.jsx)(N,{})]})}var k=a(79861),I=a(48480);function w(e){let{children:t,className:a}=e;const{isBlogPostPage:n}=(0,i.C)();return(0,d.jsx)("div",{id:n?k.blogPostContainerID:void 0,className:(0,s.Z)("markdown",a),itemProp:"articleBody",children:(0,d.jsx)(I.Z,{children:t})})}var C=a(77612),T=a(58045);function y(){return(0,d.jsx)("b",{children:(0,d.jsx)(h.Z,{id:"theme.blog.post.readMore",description:"The label used in blog post item excerpts to link to full blog posts",children:"Read More"})})}function L(e){const{blogPostTitle:t,...a}=e;return(0,d.jsx)(c.Z,{"aria-label":(0,h.I)({message:"Read more about {title}",id:"theme.blog.post.readMoreLabel",description:"The ARIA label for the link to full blog posts from excerpts"},{title:t}),...a,children:(0,d.jsx)(y,{})})}const F={blogPostFooterDetailsFull:"blogPostFooterDetailsFull_mRVl"};function B(){const{metadata:e,isBlogPostPage:t}=(0,i.C)(),{tags:a,title:n,editUrl:r,hasTruncateMarker:l}=e,o=!t&&l,c=a.length>0;return c||o||r?(0,d.jsxs)("footer",{className:(0,s.Z)("row docusaurus-mt-lg",t&&F.blogPostFooterDetailsFull),children:[c&&(0,d.jsx)("div",{className:(0,s.Z)("col",{"col--9":o}),children:(0,d.jsx)(T.Z,{tags:a})}),t&&r&&(0,d.jsx)("div",{className:"col margin-top--sm",children:(0,d.jsx)(C.Z,{editUrl:r})}),o&&(0,d.jsx)("div",{className:(0,s.Z)("col text--right",{"col--3":c}),children:(0,d.jsx)(L,{blogPostTitle:n,to:e.permalink})})]}):null}function A(e){let{children:t,className:a}=e;const n=function(){const{isBlogPostPage:e}=(0,i.C)();return e?void 0:"margin-bottom--xl"}();return(0,d.jsxs)(o.Z,{className:(0,s.Z)(n,a),children:[(0,d.jsx)(Z,{}),(0,d.jsx)(w,{children:t}),(0,d.jsx)(B,{})]})}var M=a(16948);function R(e){const{nextItem:t,prevItem:a}=e;return(0,d.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,h.I)({id:"theme.blog.post.paginator.navAriaLabel",message:"Blog post page navigation",description:"The ARIA label for the blog posts pagination"}),children:[a&&(0,d.jsx)(M.Z,{...a,subLabel:(0,d.jsx)(h.Z,{id:"theme.blog.post.paginator.newerPost",description:"The blog post button label to navigate to the newer/previous post",children:"Newer Post"})}),t&&(0,d.jsx)(M.Z,{...t,subLabel:(0,d.jsx)(h.Z,{id:"theme.blog.post.paginator.olderPost",description:"The blog post button label to navigate to the older/next post",children:"Older Post"}),isNext:!0})]})}function O(){const{assets:e,metadata:t}=(0,i.C)(),{title:a,description:s,date:r,tags:l,authors:o,frontMatter:c}=t,{keywords:m}=c,u=e.image??c.image;return(0,d.jsxs)(n.d,{title:a,description:s,keywords:m,image:u,children:[(0,d.jsx)("meta",{property:"og:type",content:"article"}),(0,d.jsx)("meta",{property:"article:published_time",content:r}),o.some((e=>e.url))&&(0,d.jsx)("meta",{property:"article:author",content:o.map((e=>e.url)).filter(Boolean).join(",")}),l.length>0&&(0,d.jsx)("meta",{property:"article:tag",content:l.map((e=>e.label)).join(",")})]})}var D=a(95967),U=a(94007);function $(e){let{sidebar:t,children:a}=e;const{metadata:s,toc:n}=(0,i.C)(),{nextItem:r,prevItem:o,frontMatter:c,unlisted:m}=s,{hide_table_of_contents:u,toc_min_heading_level:h,toc_max_heading_level:g}=c;return(0,d.jsxs)(l.Z,{sidebar:t,toc:!u&&n.length>0?(0,d.jsx)(D.Z,{toc:n,minHeadingLevel:h,maxHeadingLevel:g}):void 0,children:[m&&(0,d.jsx)(U.Z,{}),(0,d.jsx)(A,{children:a}),(r||o)&&(0,d.jsx)(R,{nextItem:r,prevItem:o})]})}function H(e){const t=e.content;return(0,d.jsx)(i.n,{content:e.content,isBlogPostPage:!0,children:(0,d.jsxs)(n.FG,{className:(0,s.Z)(r.k.wrapper.blogPages,r.k.page.blogPostPage),children:[(0,d.jsx)(O,{}),(0,d.jsx)($,{sidebar:e.sidebar,children:(0,d.jsx)(t,{})})]})})}},77612:(e,t,a)=>{a.d(t,{Z:()=>m});a(67294);var s=a(11614),n=a(18015),r=a(75013),i=a(36905);const l={iconEdit:"iconEdit_Z9Sw"};var o=a(85893);function c(e){let{className:t,...a}=e;return(0,o.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,i.Z)(l.iconEdit,t),"aria-hidden":"true",...a,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 m(e){let{editUrl:t}=e;return(0,o.jsxs)(r.Z,{to:t,className:n.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"})]})}},16948:(e,t,a)=>{a.d(t,{Z:()=>i});a(67294);var s=a(36905),n=a(75013),r=a(85893);function i(e){const{permalink:t,title:a,subLabel:i,isNext:l}=e;return(0,r.jsxs)(n.Z,{className:(0,s.Z)("pagination-nav__link",l?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[i&&(0,r.jsx)("div",{className:"pagination-nav__sublabel",children:i}),(0,r.jsx)("div",{className:"pagination-nav__label",children:a})]})}},24588:(e,t,a)=>{a.d(t,{Z:()=>l});a(67294);var s=a(36905),n=a(75013);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var i=a(85893);function l(e){let{permalink:t,label:a,count:l}=e;return(0,i.jsxs)(n.Z,{href:t,className:(0,s.Z)(r.tag,l?r.tagWithCount:r.tagRegular),children:[a,l&&(0,i.jsx)("span",{children:l})]})}},58045:(e,t,a)=>{a.d(t,{Z:()=>o});a(67294);var s=a(36905),n=a(11614),r=a(24588);const i={tags:"tags_jXut",tag:"tag_QGVx"};var l=a(85893);function o(e){let{tags:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("b",{children:(0,l.jsx)(n.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,l.jsx)("ul",{className:(0,s.Z)(i.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:a}=e;return(0,l.jsx)("li",{className:i.tag,children:(0,l.jsx)(r.Z,{label:t,permalink:a})},a)}))})]})}},17762:(e,t,a)=>{a.d(t,{C:()=>o,n:()=>l});var s=a(67294),n=a(93478),r=a(85893);const i=s.createContext(null);function l(e){let{children:t,content:a,isBlogPostPage:n=!1}=e;const l=function(e){let{content:t,isBlogPostPage:a}=e;return(0,s.useMemo)((()=>({metadata:t.metadata,frontMatter:t.frontMatter,assets:t.assets,toc:t.toc,isBlogPostPage:a})),[t,a])}({content:a,isBlogPostPage:n});return(0,r.jsx)(i.Provider,{value:l,children:t})}function o(){const e=(0,s.useContext)(i);if(null===e)throw new n.i6("BlogPostProvider");return e}},57880:(e,t,a)=>{a.d(t,{c:()=>c});var s=a(67294),n=a(6832);const r=["zero","one","two","few","many","other"];function i(e){return r.filter((t=>e.includes(t)))}const l={locale:"en",pluralForms:i(["one","other"]),select:e=>1===e?"one":"other"};function o(){const{i18n:{currentLocale:e}}=(0,n.Z)();return(0,s.useMemo)((()=>{try{return function(e){const t=new Intl.PluralRules(e);return{locale:e,pluralForms:i(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`),l}}),[e])}function c(){const e=o();return{selectMessage:(t,a)=>function(e,t,a){const s=e.split("|");if(1===s.length)return s[0];s.length>a.pluralForms.length&&console.error(`For locale=${a.locale}, a maximum of ${a.pluralForms.length} plural forms are expected (${a.pluralForms.join(",")}), but the message contains ${s.length}: ${e}`);const n=a.select(t),r=a.pluralForms.indexOf(n);return s[Math.min(r,s.length-1)]}(a,t,e)}}}}]); \ No newline at end of file diff --git a/assets/js/d1c7a4f7.9d5dfa3c.js b/assets/js/d1c7a4f7.9d5dfa3c.js new file mode 100644 index 000000000..6304eee31 --- /dev/null +++ b/assets/js/d1c7a4f7.9d5dfa3c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2469],{7443:(e,i,s)=>{s.r(i),s.d(i,{assets:()=>l,contentTitle:()=>r,default:()=>a,frontMatter:()=>d,metadata:()=>c,toc:()=>o});var t=s(85893),n=s(11151);const d={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:"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.",source:"@site/docs/pro/push_notifications.md",sourceDirName:"pro",slug:"/pro/push_notifications",permalink:"/docs/pro/push_notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/push_notifications.md",tags:[],version:"current",frontMatter:{id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},sidebar:"Pro",previous:{title:"Operation rate limits",permalink:"/docs/pro/rate_limiting"},next:{title:"SSO for admin UI (OIDC)",permalink:"/docs/pro/admin_idp_auth"}},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:"push_notifications.enable_redis_delayed_scheduler",id:"push_notificationsenable_redis_delayed_scheduler",level:4},{value:"push_notifications.dry_run",id:"push_notificationsdry_run",level:4},{value:"push_notifications.dry_run_latency",id:"push_notificationsdry_run_latency",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:"cancel_push",id:"cancel_push",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:"update_push_status",id:"update_push_status",level:3},{value:"update_push_status request",id:"update_push_status-request-1",level:4},{value:"update_push_status result",id:"update_push_status-result-1",level:4},{value:"Exposed metrics",id:"exposed-metrics",level:2},{value:"centrifugo_push_notification_count",id:"centrifugo_push_notification_count",level:4},{value:"centrifugo_push_queue_consuming_lag",id:"centrifugo_push_queue_consuming_lag",level:4},{value:"centrifugo_push_consuming_inflight_jobs",id:"centrifugo_push_consuming_inflight_jobs",level:4},{value:"centrifugo_push_job_duration_seconds",id:"centrifugo_push_job_duration_seconds",level:4},{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,n.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(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:"FCM, HMS, APNs handle the frontend and transport aspects of notification delivery. Device token storage, management and efficient push notification broadcasting is managed by Centrifugo PRO. Tokens are stored in a PostgreSQL database. To facilitate efficient push notification broadcasting towards devices, Centrifugo PRO includes worker queues based on Redis streams (and also provides and option to use PostgreSQL-based queue)."}),"\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/send_push_notification \\\n-H "Authorization: apikey " \\\n-d @- <<\'EOF\'\n\n{\n "recipient": {\n "filter": {\n "topics": ["test"]\n }\n },\n "notification": {\n "fcm": {\n "message": {\n "notification": {"title": "Hello", "body": "How are you?"}\n }\n }\n }\n}\nEOF\n'})}),"\n",(0,t.jsx)(i.p,{children:"In addition, Centrifugo PRO includes a helpful web UI for inspecting registered devices and sending push notifications:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{src:s(24088).Z+"",width:"2728",height:"1094"})}),"\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 several millions pushes per minute."}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO also supports delayed push notifications feature \u2013 to queue push for a later delivery, so for example you can send notification based on user time zone and let Centrifugo PRO send it when needed."}),"\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/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": " ",\n "hms_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": " ",\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 integer option configures the number of days to keep device without updates. By default Centrifugo does not remove inactive devices."}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsenable_redis_delayed_scheduler",children:"push_notifications.enable_redis_delayed_scheduler"}),"\n",(0,t.jsx)(i.p,{children:"Boolean option which enables Redis scheduler to process delayed push notifications. It's off by default since produces additional requests to Redis. When using PostgreSQL as push notifications queue engine you don't need to enable sheduler explicitly."}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsdry_run",children:"push_notifications.dry_run"}),"\n",(0,t.jsxs)(i.p,{children:["Boolean option, when ",(0,t.jsx)(i.code,{children:"true"})," Centrifugo PRO does not send push notifications to FCM, APNs, HMS providers but instead just print logs. Useful for development."]}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsdry_run_latency",children:"push_notifications.dry_run_latency"}),"\n",(0,t.jsxs)(i.p,{children:["Duration. When set together with ",(0,t.jsx)(i.code,{children:"push_notifications.dry_run"})," every dry-run request will cause some delay in workers emulating real-world latency. Useful for development."]}),"\n",(0,t.jsx)(i.h3,{id:"use-postgresql-as-queue",children:"Use PostgreSQL as queue"}),"\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 generally more efficient, so if you start with PostgreSQL based queue \u2013 you have an option to switch to a more performant implementation later. Though in-flight and currently queued 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:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map "})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional custom metadata for the device"})]})]})]}),"\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:"user_update"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{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:(0,t.jsx)(i.code,{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:"topics_update"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceTopicsUpdate"})}),(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 "})}),(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:"DeviceTopicsUpdate"}),":"]}),"\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:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Operation to make: ",(0,t.jsx)(i.code,{children:"add"}),", ",(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:"Yes"}),(0,t.jsx)(i.td,{children:"Topics for the operation"})]})]})]}),"\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"})]})]})]}),"\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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceFilter"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"How to filter results"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"cursor"})}),(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_total_count"})}),(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 total count for the current filter."})]}),(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_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.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceFilter"}),":"]}),"\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:"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:"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."})]})]})]}),"\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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor string for retreiving the next page, if not set - then no next page exists"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Total count value (if ",(0,t.jsx)(i.code,{children:"include_total_count"})," used)"]})]})]})]}),"\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:"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:"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:"Yes"}),(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:"Yes"}),(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:"Yes"}),(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:"No"}),(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.jsx)(i.td,{children:"No"}),(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:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map "})}),(0,t.jsx)(i.td,{children:"No"}),(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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceTopicFilter"})}),(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:"cursor"})}),(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."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_total_count"})}),(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 total count info to response."})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTopicFilter"}),":"]}),"\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_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:"Topic prefix to filter results."})]})]})]}),"\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:"DeviceTopic"})]}),(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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor string for retreiving the next page, if not set - then no next page exists"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Total count value (if ",(0,t.jsx)(i.code,{children:"include_total_count"})," used)"]})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTopic"}),":"]}),"\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 DeviceTopic object"})]}),(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:"Topic"})]})]})]}),"\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:"flter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"UserTopicFilter"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Filter object."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"cursor"})}),(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."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_total_count"})}),(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 total count info to response."})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"UserTopicFilter"}),":"]}),"\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."})]})]})]}),"\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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"})]})]})]}),"\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:"Topic"})]})]})]}),"\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"})]}),(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 or to cancel delayed push. We recommend using UUID v4 for it"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"send_at"})}),(0,t.jsx)(i.td,{children:"int64"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional Unix time in the future (in seconds) when to send push notification, push will be queued until that time."})]})]})]}),"\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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceFilter"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to device IDs based on Centrifugo device storage filter"})]}),(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:"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. Note, it's Centrifugo specific and does not relate to notification TTL fields. We generally recommend to always set this to a reasonable value to protect your app from old push notifications sending"})]}),(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 "})}),(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:"cancel_push",children:"cancel_push"}),"\n",(0,t.jsxs)(i.p,{children:["Cancel delayed push notification (which was sent with custom ",(0,t.jsx)(i.code,{children:"send_at"})," value)."]}),"\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.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.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"uid"})," of push notification to cancel"]})]})})]}),"\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.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/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-1",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-1",children:"update_push_status result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h2,{id:"exposed-metrics",children:"Exposed metrics"}),"\n",(0,t.jsx)(i.p,{children:"Several metrics are available to monitor the state of Centrifugo push worker system:"}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_notification_count",children:"centrifugo_push_notification_count"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Counter"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, recipient_type, platform, success, err_code"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Total count of push notifications."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Helps in tracking the number and success rate of push notifications sent, providing insights for optimization and troubleshooting."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_queue_consuming_lag",children:"centrifugo_push_queue_consuming_lag"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Gauge"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, queue"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Queue consuming lag in seconds."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Useful for monitoring the delay in processing jobs from the queue, helping identify potential bottlenecks and ensuring timely processing."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_consuming_inflight_jobs",children:"centrifugo_push_consuming_inflight_jobs"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Gauge"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, queue"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Number of inflight jobs being consumed."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Helps in tracking the load on the job processing system, ensuring that resources are being utilized efficiently."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_job_duration_seconds",children:"centrifugo_push_job_duration_seconds"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, recipient_type"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Duration of push processing job in seconds."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Useful for monitoring the performance of job processing, helping in performance tuning and issue resolution."]}),"\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,n.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"},24088:(e,i,s)=>{s.d(i,{Z:()=>t});const t=s.p+"assets/images/push_ui-01989161306e71d882a064b605395dcb.png"},11151:(e,i,s)=>{s.d(i,{Z:()=>c,a:()=>r});var t=s(67294);const n={},d=t.createContext(n);function r(e){const i=t.useContext(d);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(n):e.components||n:r(e.components),t.createElement(d.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d1c7a4f7.f607af94.js b/assets/js/d1c7a4f7.f607af94.js deleted file mode 100644 index d6eb33152..000000000 --- a/assets/js/d1c7a4f7.f607af94.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[2469],{7443:(e,i,s)=>{s.r(i),s.d(i,{assets:()=>l,contentTitle:()=>r,default:()=>a,frontMatter:()=>d,metadata:()=>c,toc:()=>o});var t=s(85893),n=s(11151);const d={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:"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.",source:"@site/docs/pro/push_notifications.md",sourceDirName:"pro",slug:"/pro/push_notifications",permalink:"/docs/pro/push_notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/push_notifications.md",tags:[],version:"current",frontMatter:{id:"push_notifications",sidebar_label:"Push notification API",title:"Push notification API"},sidebar:"Pro",previous:{title:"Operation rate limits",permalink:"/docs/pro/rate_limiting"},next:{title:"User status API",permalink:"/docs/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:"push_notifications.enable_redis_delayed_scheduler",id:"push_notificationsenable_redis_delayed_scheduler",level:4},{value:"push_notifications.dry_run",id:"push_notificationsdry_run",level:4},{value:"push_notifications.dry_run_latency",id:"push_notificationsdry_run_latency",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:"cancel_push",id:"cancel_push",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:"update_push_status",id:"update_push_status",level:3},{value:"update_push_status request",id:"update_push_status-request-1",level:4},{value:"update_push_status result",id:"update_push_status-result-1",level:4},{value:"Exposed metrics",id:"exposed-metrics",level:2},{value:"centrifugo_push_notification_count",id:"centrifugo_push_notification_count",level:4},{value:"centrifugo_push_queue_consuming_lag",id:"centrifugo_push_queue_consuming_lag",level:4},{value:"centrifugo_push_consuming_inflight_jobs",id:"centrifugo_push_consuming_inflight_jobs",level:4},{value:"centrifugo_push_job_duration_seconds",id:"centrifugo_push_job_duration_seconds",level:4},{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,n.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(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:"FCM, HMS, APNs handle the frontend and transport aspects of notification delivery. Device token storage, management and efficient push notification broadcasting is managed by Centrifugo PRO. Tokens are stored in a PostgreSQL database. To facilitate efficient push notification broadcasting towards devices, Centrifugo PRO includes worker queues based on Redis streams (and also provides and option to use PostgreSQL-based queue)."}),"\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/send_push_notification \\\n-H "Authorization: apikey " \\\n-d @- <<\'EOF\'\n\n{\n "recipient": {\n "filter": {\n "topics": ["test"]\n }\n },\n "notification": {\n "fcm": {\n "message": {\n "notification": {"title": "Hello", "body": "How are you?"}\n }\n }\n }\n}\nEOF\n'})}),"\n",(0,t.jsx)(i.p,{children:"In addition, Centrifugo PRO includes a helpful web UI for inspecting registered devices and sending push notifications:"}),"\n",(0,t.jsx)(i.p,{children:(0,t.jsx)(i.img,{src:s(24088).Z+"",width:"2728",height:"1094"})}),"\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 several millions pushes per minute."}),"\n",(0,t.jsx)(i.p,{children:"Centrifugo PRO also supports delayed push notifications feature \u2013 to queue push for a later delivery, so for example you can send notification based on user time zone and let Centrifugo PRO send it when needed."}),"\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/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": " ",\n "hms_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": " ",\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 integer option configures the number of days to keep device without updates. By default Centrifugo does not remove inactive devices."}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsenable_redis_delayed_scheduler",children:"push_notifications.enable_redis_delayed_scheduler"}),"\n",(0,t.jsx)(i.p,{children:"Boolean option which enables Redis scheduler to process delayed push notifications. It's off by default since produces additional requests to Redis. When using PostgreSQL as push notifications queue engine you don't need to enable sheduler explicitly."}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsdry_run",children:"push_notifications.dry_run"}),"\n",(0,t.jsxs)(i.p,{children:["Boolean option, when ",(0,t.jsx)(i.code,{children:"true"})," Centrifugo PRO does not send push notifications to FCM, APNs, HMS providers but instead just print logs. Useful for development."]}),"\n",(0,t.jsx)(i.h4,{id:"push_notificationsdry_run_latency",children:"push_notifications.dry_run_latency"}),"\n",(0,t.jsxs)(i.p,{children:["Duration. When set together with ",(0,t.jsx)(i.code,{children:"push_notifications.dry_run"})," every dry-run request will cause some delay in workers emulating real-world latency. Useful for development."]}),"\n",(0,t.jsx)(i.h3,{id:"use-postgresql-as-queue",children:"Use PostgreSQL as queue"}),"\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 generally more efficient, so if you start with PostgreSQL based queue \u2013 you have an option to switch to a more performant implementation later. Though in-flight and currently queued 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:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map "})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Additional custom metadata for the device"})]})]})]}),"\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:"user_update"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{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:(0,t.jsx)(i.code,{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:"topics_update"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceTopicsUpdate"})}),(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 "})}),(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:"DeviceTopicsUpdate"}),":"]}),"\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:"op"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:["Operation to make: ",(0,t.jsx)(i.code,{children:"add"}),", ",(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:"Yes"}),(0,t.jsx)(i.td,{children:"Topics for the operation"})]})]})]}),"\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"})]})]})]}),"\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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceFilter"})}),(0,t.jsx)(i.td,{children:"Yes"}),(0,t.jsx)(i.td,{children:"How to filter results"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"cursor"})}),(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_total_count"})}),(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 total count for the current filter."})]}),(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_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.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceFilter"}),":"]}),"\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:"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:"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."})]})]})]}),"\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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor string for retreiving the next page, if not set - then no next page exists"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Total count value (if ",(0,t.jsx)(i.code,{children:"include_total_count"})," used)"]})]})]})]}),"\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:"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:"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:"Yes"}),(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:"Yes"}),(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:"Yes"}),(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:"No"}),(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.jsx)(i.td,{children:"No"}),(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:"meta"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"map "})}),(0,t.jsx)(i.td,{children:"No"}),(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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceTopicFilter"})}),(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:"cursor"})}),(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."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_total_count"})}),(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 total count info to response."})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTopicFilter"}),":"]}),"\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_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:"Topic prefix to filter results."})]})]})]}),"\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:"DeviceTopic"})]}),(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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Cursor string for retreiving the next page, if not set - then no next page exists"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsxs)(i.td,{children:["Total count value (if ",(0,t.jsx)(i.code,{children:"include_total_count"})," used)"]})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"DeviceTopic"}),":"]}),"\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 DeviceTopic object"})]}),(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:"Topic"})]})]})]}),"\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:"flter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"UserTopicFilter"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Filter object."})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"cursor"})}),(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."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"include_total_count"})}),(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 total count info to response."})]})]})]}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"UserTopicFilter"}),":"]}),"\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."})]})]})]}),"\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:"next_cursor"})}),(0,t.jsx)(i.td,{children:"string"}),(0,t.jsx)(i.td,{children:"No"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"total_count"})}),(0,t.jsx)(i.td,{children:"integer"}),(0,t.jsx)(i.td,{children:"No"})]})]})]}),"\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:"Topic"})]})]})]}),"\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"})]}),(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 or to cancel delayed push. We recommend using UUID v4 for it"})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"send_at"})}),(0,t.jsx)(i.td,{children:"int64"}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Optional Unix time in the future (in seconds) when to send push notification, push will be queued until that time."})]})]})]}),"\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:"filter"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DeviceFilter"})}),(0,t.jsx)(i.td,{children:"No"}),(0,t.jsx)(i.td,{children:"Send to device IDs based on Centrifugo device storage filter"})]}),(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:"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. Note, it's Centrifugo specific and does not relate to notification TTL fields. We generally recommend to always set this to a reasonable value to protect your app from old push notifications sending"})]}),(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 "})}),(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:"cancel_push",children:"cancel_push"}),"\n",(0,t.jsxs)(i.p,{children:["Cancel delayed push notification (which was sent with custom ",(0,t.jsx)(i.code,{children:"send_at"})," value)."]}),"\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.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.jsx)(i.td,{children:"Yes"}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"uid"})," of push notification to cancel"]})]})})]}),"\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.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/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-1",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-1",children:"update_push_status result"}),"\n",(0,t.jsx)(i.p,{children:"Empty object."}),"\n",(0,t.jsx)(i.h2,{id:"exposed-metrics",children:"Exposed metrics"}),"\n",(0,t.jsx)(i.p,{children:"Several metrics are available to monitor the state of Centrifugo push worker system:"}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_notification_count",children:"centrifugo_push_notification_count"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Counter"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, recipient_type, platform, success, err_code"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Total count of push notifications."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Helps in tracking the number and success rate of push notifications sent, providing insights for optimization and troubleshooting."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_queue_consuming_lag",children:"centrifugo_push_queue_consuming_lag"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Gauge"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, queue"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Queue consuming lag in seconds."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Useful for monitoring the delay in processing jobs from the queue, helping identify potential bottlenecks and ensuring timely processing."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_consuming_inflight_jobs",children:"centrifugo_push_consuming_inflight_jobs"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Gauge"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, queue"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Number of inflight jobs being consumed."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Helps in tracking the load on the job processing system, ensuring that resources are being utilized efficiently."]}),"\n"]}),"\n",(0,t.jsx)(i.h4,{id:"centrifugo_push_job_duration_seconds",children:"centrifugo_push_job_duration_seconds"}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Type:"})," Summary"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Labels:"})," provider, recipient_type"]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Description:"})," Duration of push processing job in seconds."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.strong,{children:"Usage:"})," Useful for monitoring the performance of job processing, helping in performance tuning and issue resolution."]}),"\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,n.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"},24088:(e,i,s)=>{s.d(i,{Z:()=>t});const t=s.p+"assets/images/push_ui-01989161306e71d882a064b605395dcb.png"},11151:(e,i,s)=>{s.d(i,{Z:()=>c,a:()=>r});var t=s(67294);const n={},d=t.createContext(n);function r(e){const i=t.useContext(d);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(n):e.components||n:r(e.components),t.createElement(d.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d2c1944d.0b4b53d6.js b/assets/js/d2c1944d.0b4b53d6.js new file mode 100644 index 000000000..1b077796e --- /dev/null +++ b/assets/js/d2c1944d.0b4b53d6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1695],{84045:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>l,toc:()=>c});var a=t(85893),s=t(11151);const i={id:"channel_patterns",sidebar_label:"Channel patterns",title:"Channel patterns"},r=void 0,l={id:"pro/channel_patterns",title:"Channel patterns",description:"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.",source:"@site/docs/pro/channel_patterns.md",sourceDirName:"pro",slug:"/pro/channel_patterns",permalink:"/docs/pro/channel_patterns",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/channel_patterns.md",tags:[],version:"current",frontMatter:{id:"channel_patterns",sidebar_label:"Channel patterns",title:"Channel patterns"},sidebar:"Pro",previous:{title:"Channel capabilities",permalink:"/docs/pro/capabilities"},next:{title:"Channel CEL expressions",permalink:"/docs/pro/cel_expressions"}},o={},c=[{value:"Configuration",id:"configuration",level:3},{value:"Implementation details",id:"implementation-details",level:3},{value:"Variables",id:"variables",level:3},{value:"Using varibles",id:"using-varibles",level:3}];function h(e){const n={a:"a",code:"code",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(n.h3,{id:"configuration",children:"Configuration"}),"\n",(0,a.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-json",children:'{\n // rest of the config ...\n "channel_patterns": true, // required to turn on the feature.\n "namespaces": [\n {\n "name": "/users/:name"\n // namespace options may go here ...\n },\n {\n "name": "/events/:project/:type"\n // namespace options may go here ...\n }\n ]\n}\n'})}),"\n",(0,a.jsxs)(n.p,{children:["As soon as namespace name starts with ",(0,a.jsx)(n.code,{children:"/"})," - it's considered a channel pattern. Just like an HTTP path it consists of segments delimited by ",(0,a.jsx)(n.code,{children:"/"}),". The ",(0,a.jsx)(n.code,{children:":"})," symbol in the segment beginning defines a variable part \u2013 more information below."]}),"\n",(0,a.jsxs)(n.p,{children:["In this case a channel to be used must be sth like ",(0,a.jsx)(n.code,{children:"/users/mario"})," - i.e. start with ",(0,a.jsx)(n.code,{children:"/"})," and match one of the patterns defined in the configuration. So this channel pattern matching mechanics behaves mostly like HTTP route matching in many frameworks."]}),"\n",(0,a.jsx)(n.p,{children:"Given the configuration example above:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/users/mario"}),", then the namespace with the name ",(0,a.jsx)(n.code,{children:"/users/:name"})," will match and we apply all the options defined for it to the channel."]}),"\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/events/42/news"}),", then the namespace with the name ",(0,a.jsx)(n.code,{children:"/events/:project/:type"})," will match."]}),"\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/events/42"}),", then no namespace will match and the ",(0,a.jsx)(n.code,{children:"unknown channel"})," error will be returned."]}),"\n"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-javascript",metastring:'title="Basic example demonstrating use of pattern channels in JS"',children:"const client := new Centrifuge(\"ws://...\", {});\nconst sub = client.newSubscription('/users/mario');\nsub.subscribe();\nclient.connect();\n"})}),"\n",(0,a.jsx)(n.h3,{id:"implementation-details",children:"Implementation details"}),"\n",(0,a.jsx)(n.p,{children:"Some implementation restrictions and details to know about:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["When using channel patterns feature ",(0,a.jsx)(n.code,{children:":"})," symbol in a namespace name defines a variable part. It's not related to a namespace separator anymore \u2013 the entire channel is matched over the channel pattern. Similar to the HTTP routes semantics. So namespace separator is not needed at all when using channel patterns."]}),"\n",(0,a.jsx)(n.li,{children:"Centrifugo only allows explicit channel pattern matching which do not result into channel pattern conflicts in runtime, this is checked during configuration validation on server start. Explicitly defined static patterns (without variables) have precedence over patterns with variables."}),"\n",(0,a.jsxs)(n.li,{children:["There is no analogue of top-level namespace (like we have for standard namespace configuration) for channels starting with ",(0,a.jsx)(n.code,{children:"/"}),". If a channel does not match any explicitly defined pattern then Centrifugo returns the ",(0,a.jsx)(n.code,{children:"102: unknown channel"})," error."]}),"\n",(0,a.jsxs)(n.li,{children:["If you define ",(0,a.jsx)(n.code,{children:"channel_regex"})," inside channel pattern options \u2013 then regex matches over the entire channel (since variable parts are located in the namespace name in this case)."]}),"\n",(0,a.jsx)(n.li,{children:"Channel pattern must only contain ASCII characters."}),"\n",(0,a.jsxs)(n.li,{children:["Duplicate variable names are not allowed inside an individual pattern, i.e. defining ",(0,a.jsx)(n.code,{children:"/users/:user/:user"})," will result into validation error on start."]}),"\n"]}),"\n",(0,a.jsx)(n.h3,{id:"variables",children:"Variables"}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.code,{children:":"})," in the channel pattern name helps to define a variable to match against. Named parameters only match a single segment of the channel:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/users/:name":\n\n/users/mary \u2705 match\n/users/john \u2705 match\n/users/mary/info \u274c no match \n/users \u274c no match\n'})}),"\n",(0,a.jsxs)(n.p,{children:["Another example for channel pattern ",(0,a.jsx)(n.code,{children:"/news/:type/:subtype"}),", i.e. with multiple variables:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/news/:type/:subtype":\n\n/news/sport/football \u2705 match\n/news/sport/volleyball \u2705 match\n/news/sport \u274c no match\n/news \u274c no match\n'})}),"\n",(0,a.jsx)(n.p,{children:"Channel patterns support mid-segment variables, so the following is possible:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/personal/user_:user":\n\n/personal/user_mary \u2705 match\n/personal/user_john \u2705 match\n/personal/user_ \u274c no match\n'})}),"\n",(0,a.jsx)(n.h3,{id:"using-varibles",children:"Using varibles"}),"\n",(0,a.jsxs)(n.p,{children:["Additional benefits of using channel patterns may be achieved together with Centrifugo PRO ",(0,a.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"CEL expressions"}),". Channel pattern variables are available inside CEL expressions for evaluation in a custom way."]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},11151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var a=t(67294);const s={},i=a.createContext(s);function r(e){const n=a.useContext(i);return a.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),a.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d2c1944d.a5b7b2e8.js b/assets/js/d2c1944d.a5b7b2e8.js deleted file mode 100644 index 226e81cb3..000000000 --- a/assets/js/d2c1944d.a5b7b2e8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[1695],{84045:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>r,default:()=>d,frontMatter:()=>i,metadata:()=>l,toc:()=>c});var a=t(85893),s=t(11151);const i={id:"channel_patterns",sidebar_label:"Channel patterns",title:"Channel patterns"},r=void 0,l={id:"pro/channel_patterns",title:"Channel patterns",description:"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.",source:"@site/docs/pro/channel_patterns.md",sourceDirName:"pro",slug:"/pro/channel_patterns",permalink:"/docs/pro/channel_patterns",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/channel_patterns.md",tags:[],version:"current",frontMatter:{id:"channel_patterns",sidebar_label:"Channel patterns",title:"Channel patterns"},sidebar:"Pro",previous:{title:"Channel capabilities",permalink:"/docs/pro/capabilities"},next:{title:"CEL expressions",permalink:"/docs/pro/cel_expressions"}},o={},c=[{value:"Configuration",id:"configuration",level:3},{value:"Implementation details",id:"implementation-details",level:3},{value:"Variables",id:"variables",level:3},{value:"Using varibles",id:"using-varibles",level:3}];function h(e){const n={a:"a",code:"code",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.p,{children:"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."}),"\n",(0,a.jsx)(n.h3,{id:"configuration",children:"Configuration"}),"\n",(0,a.jsx)(n.p,{children:"Let's look at the example:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-json",children:'{\n // rest of the config ...\n "channel_patterns": true, // required to turn on the feature.\n "namespaces": [\n {\n "name": "/users/:name"\n // namespace options may go here ...\n },\n {\n "name": "/events/:project/:type"\n // namespace options may go here ...\n }\n ]\n}\n'})}),"\n",(0,a.jsxs)(n.p,{children:["As soon as namespace name starts with ",(0,a.jsx)(n.code,{children:"/"})," - it's considered a channel pattern. Just like an HTTP path it consists of segments delimited by ",(0,a.jsx)(n.code,{children:"/"}),". The ",(0,a.jsx)(n.code,{children:":"})," symbol in the segment beginning defines a variable part \u2013 more information below."]}),"\n",(0,a.jsxs)(n.p,{children:["In this case a channel to be used must be sth like ",(0,a.jsx)(n.code,{children:"/users/mario"})," - i.e. start with ",(0,a.jsx)(n.code,{children:"/"})," and match one of the patterns defined in the configuration. So this channel pattern matching mechanics behaves mostly like HTTP route matching in many frameworks."]}),"\n",(0,a.jsx)(n.p,{children:"Given the configuration example above:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/users/mario"}),", then the namespace with the name ",(0,a.jsx)(n.code,{children:"/users/:name"})," will match and we apply all the options defined for it to the channel."]}),"\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/events/42/news"}),", then the namespace with the name ",(0,a.jsx)(n.code,{children:"/events/:project/:type"})," will match."]}),"\n",(0,a.jsxs)(n.li,{children:["if channel is ",(0,a.jsx)(n.code,{children:"/events/42"}),", then no namespace will match and the ",(0,a.jsx)(n.code,{children:"unknown channel"})," error will be returned."]}),"\n"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-javascript",metastring:'title="Basic example demonstrating use of pattern channels in JS"',children:"const client := new Centrifuge(\"ws://...\", {});\nconst sub = client.newSubscription('/users/mario');\nsub.subscribe();\nclient.connect();\n"})}),"\n",(0,a.jsx)(n.h3,{id:"implementation-details",children:"Implementation details"}),"\n",(0,a.jsx)(n.p,{children:"Some implementation restrictions and details to know about:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsxs)(n.li,{children:["When using channel patterns feature ",(0,a.jsx)(n.code,{children:":"})," symbol in a namespace name defines a variable part. It's not related to a namespace separator anymore \u2013 the entire channel is matched over the channel pattern. Similar to the HTTP routes semantics. So namespace separator is not needed at all when using channel patterns."]}),"\n",(0,a.jsx)(n.li,{children:"Centrifugo only allows explicit channel pattern matching which do not result into channel pattern conflicts in runtime, this is checked during configuration validation on server start. Explicitly defined static patterns (without variables) have precedence over patterns with variables."}),"\n",(0,a.jsxs)(n.li,{children:["There is no analogue of top-level namespace (like we have for standard namespace configuration) for channels starting with ",(0,a.jsx)(n.code,{children:"/"}),". If a channel does not match any explicitly defined pattern then Centrifugo returns the ",(0,a.jsx)(n.code,{children:"102: unknown channel"})," error."]}),"\n",(0,a.jsxs)(n.li,{children:["If you define ",(0,a.jsx)(n.code,{children:"channel_regex"})," inside channel pattern options \u2013 then regex matches over the entire channel (since variable parts are located in the namespace name in this case)."]}),"\n",(0,a.jsx)(n.li,{children:"Channel pattern must only contain ASCII characters."}),"\n",(0,a.jsxs)(n.li,{children:["Duplicate variable names are not allowed inside an individual pattern, i.e. defining ",(0,a.jsx)(n.code,{children:"/users/:user/:user"})," will result into validation error on start."]}),"\n"]}),"\n",(0,a.jsx)(n.h3,{id:"variables",children:"Variables"}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.code,{children:":"})," in the channel pattern name helps to define a variable to match against. Named parameters only match a single segment of the channel:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/users/:name":\n\n/users/mary \u2705 match\n/users/john \u2705 match\n/users/mary/info \u274c no match \n/users \u274c no match\n'})}),"\n",(0,a.jsxs)(n.p,{children:["Another example for channel pattern ",(0,a.jsx)(n.code,{children:"/news/:type/:subtype"}),", i.e. with multiple variables:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/news/:type/:subtype":\n\n/news/sport/football \u2705 match\n/news/sport/volleyball \u2705 match\n/news/sport \u274c no match\n/news \u274c no match\n'})}),"\n",(0,a.jsx)(n.p,{children:"Channel patterns support mid-segment variables, so the following is possible:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:'Channel pattern "/personal/user_:user":\n\n/personal/user_mary \u2705 match\n/personal/user_john \u2705 match\n/personal/user_ \u274c no match\n'})}),"\n",(0,a.jsx)(n.h3,{id:"using-varibles",children:"Using varibles"}),"\n",(0,a.jsxs)(n.p,{children:["Additional benefits of using channel patterns may be achieved together with Centrifugo PRO ",(0,a.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"CEL expressions"}),". Channel pattern variables are available inside CEL expressions for evaluation in a custom way."]})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},11151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var a=t(67294);const s={},i=a.createContext(s);function r(e){const n=a.useContext(i);return a.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),a.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d2fe6fea.1b2ae3e9.js b/assets/js/d2fe6fea.1b2ae3e9.js deleted file mode 100644 index 8b6300e71..000000000 --- a/assets/js/d2fe6fea.1b2ae3e9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5901],{75467:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>u});var t=i(85893),s=i(11151),r=i(74866),o=i(85162);const a={id:"channel_token_auth",title:"Channel JWT authorization"},c=void 0,l={id:"server/channel_token_auth",title:"Channel JWT authorization",description:"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.",source:"@site/docs/server/channel_token_auth.md",sourceDirName:"server",slug:"/server/channel_token_auth",permalink:"/docs/server/channel_token_auth",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/channel_token_auth.md",tags:[],version:"current",frontMatter:{id:"channel_token_auth",title:"Channel JWT authorization"},sidebar:"Guides",previous:{title:"Channel permission model",permalink:"/docs/server/channel_permissions"},next:{title:"Server-side subscriptions",permalink:"/docs/server/server_subs"}},d={},u=[{value:"Subscription JWT claims",id:"subscription-jwt-claims",level:2},{value:"sub",id:"sub",level:3},{value:"channel",id:"channel",level:3},{value:"info",id:"info",level:3},{value:"b64info",id:"b64info",level:3},{value:"exp",id:"exp",level:3},{value:"expire_at",id:"expire_at",level:3},{value:"aud",id:"aud",level:3},{value:"iss",id:"iss",level:3},{value:"iat",id:"iat",level:3},{value:"jti",id:"jti",level:3},{value:"override",id:"override",level:3},{value:"Example",id:"example",level:2},{value:"gensubtoken cli command",id:"gensubtoken-cli-command",level:2},{value:"Separate subscription token config",id:"separate-subscription-token-config",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",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,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["In the chapter about ",(0,t.jsx)(n.a,{href:"/docs/server/channel_permissions",children:"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."]}),"\n",(0,t.jsxs)(n.p,{children:["Subscription token is also JWT. Very similar to ",(0,t.jsx)(n.a,{href:"/docs/server/authentication",children:"connection token"}),", but with specific custom claims."]}),"\n",(0,t.jsx)(n.p,{children:"Valid subscription token passed to Centrifugo in subscribe request will tell Centrifugo that subscription must be accepted."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(30841).Z+"",width:"3062",height:"1173"})}),"\n",(0,t.jsxs)(n.p,{children:["See more info about working with subscription tokens on the client side in ",(0,t.jsx)(n.a,{href:"/docs/transports/client_api#subscription-token",children:"client SDK spec"}),"."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Connection token and subscription token are both JWT and both can be generated with any JWT library."})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Even when authorizing a subscription to a channel with a subscription JWT you should still set a proper connection JWT for a client as it provides user authentication details to Centrifugo."})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Just like connection JWT using subscription JWT with a reasonable expiration time may help you have a good level of security in channels and still survive massive reconnect scenario \u2013 when many clients resubscribe alltogether."})}),"\n",(0,t.jsx)(n.p,{children:"Supported JWT algorithms for private subscription tokens match algorithms to create connection JWT. The same HMAC secret key, RSA, and ECDSA public keys set for authentication tokens are re-used to check subscription JWT."}),"\n",(0,t.jsx)(n.h2,{id:"subscription-jwt-claims",children:"Subscription JWT claims"}),"\n",(0,t.jsxs)(n.p,{children:["For subscription JWT Centrifugo uses some standard claims defined in ",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"rfc7519"}),", also some custom Centrifugo-specific."]}),"\n",(0,t.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,t.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,t.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,t.jsx)(n.p,{children:"The value must match a user in connection JWT \u2013 since it's the same real-time connection. The missing claim will mean that token issued for anonymous user (i.e. with empty user ID)."}),"\n",(0,t.jsx)(n.h3,{id:"channel",children:"channel"}),"\n",(0,t.jsxs)(n.p,{children:["Required. Channel that client tries to subscribe to with this token (",(0,t.jsx)(n.strong,{children:"string"}),")."]}),"\n",(0,t.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. Additional information for connection inside this channel (",(0,t.jsx)(n.strong,{children:"valid JSON"}),")."]}),"\n",(0,t.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. Additional information for connection inside this channel in base64 format (",(0,t.jsx)(n.strong,{children:"string"}),"). Will be decoded by Centrifugo to raw bytes."]}),"\n",(0,t.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,t.jsx)(n.p,{children:"Optional. This is a standard JWT claim that allows setting private channel subscription token expiration time (a UNIX timestamp in the future, in seconds, as integer) and configures subscription expiration time."}),"\n",(0,t.jsxs)(n.p,{children:["At the moment if the subscription expires client connection will be closed and the client will try to reconnect. In most cases, you don't need this and should prefer using the expiration of the connection JWT to deactivate the connection (see ",(0,t.jsx)(n.a,{href:"/docs/server/authentication",children:"authentication"}),"). But if you need more granular per-channel control this may fit your needs."]}),"\n",(0,t.jsxs)(n.p,{children:["Once ",(0,t.jsx)(n.code,{children:"exp"})," is set in token every subscription token must be periodically refreshed. This refresh workflow happens on the client side. Refer to the specific client documentation to see how to refresh subscriptions."]}),"\n",(0,t.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. By default, Centrifugo looks on ",(0,t.jsx)(n.code,{children:"exp"})," claim to both check token expiration and configure subscription expiration time. In most cases this is fine, but there could be situations where you want to decouple subscription token expiration check with subscription expiration time. As soon as the ",(0,t.jsx)(n.code,{children:"expire_at"})," claim is provided (set) in subscription JWT Centrifugo relies on it for setting subscription expiration time (JWT expiration still checked over ",(0,t.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the subscription should expire."]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Set it to the future time for expiring subscription at some point"}),"\n",(0,t.jsxs)(n.li,{children:["Set it to ",(0,t.jsx)(n.code,{children:"0"})," to disable subscription expiration (but still check token ",(0,t.jsx)(n.code,{children:"exp"})," claim). This allows implementing a one-time subscription token."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim). But if you set ",(0,t.jsx)(n.code,{children:"token_audience"})," option as described in ",(0,t.jsx)(n.a,{href:"/docs/server/authentication#aud",children:"client authentication"})," then audience for subscription JWT will also be checked."]}),"\n",(0,t.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim). But if you set ",(0,t.jsx)(n.code,{children:"token_issuer"})," option as described in ",(0,t.jsx)(n.a,{href:"/docs/server/authentication#iss",children:"client authentication"})," then issuer for subscription JWT will also be checked."]}),"\n",(0,t.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,t.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,t.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,t.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,t.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,t.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,t.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"override",children:"override"}),"\n",(0,t.jsxs)(n.p,{children:["One more claim is ",(0,t.jsx)(n.code,{children:"override"}),". This is an object which allows overriding channel options for the particular channel subscriber which comes with subscription token."]}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"presence"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"presence"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"join_leave"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_push_join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_push_join_leave"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_recovery"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_recovery"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_positioning"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_positioning"})," channel option"]})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BoolValue"})," is an object like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"So for example, you want to turn off emitting a presence information for a particular subscriber in a channel:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n ...\n "override": {\n "presence": {\n "value": false\n }\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"example",children:"Example"}),"\n",(0,t.jsxs)(n.p,{children:["So to generate a subscription token you can use something like this in Python (assuming user ID is ",(0,t.jsx)(n.code,{children:"42"})," and the channel is ",(0,t.jsx)(n.code,{children:"gossips"}),"):"]}),"\n","\n","\n",(0,t.jsxs)(r.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(o.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {"sub": "42", "channel": "$gossips", "exp": int(time.time()) + 3600}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,t.jsx)(o.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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: '42', channel: '$gossips' })\n .setProtectedHeader({ alg })\n .setExpirationTime('1h')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,t.jsxs)(n.p,{children:["Where ",(0,t.jsx)(n.code,{children:'"secret"'})," is the ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo configuration (we use HMAC tokens in this example which relies on a shared secret key, for RSA or ECDSA tokens you need to use a private key known only by your backend)."]}),"\n",(0,t.jsx)(n.h2,{id:"gensubtoken-cli-command",children:"gensubtoken cli command"}),"\n",(0,t.jsxs)(n.p,{children:["During development you can quickly generate valid subscription token using Centrifugo ",(0,t.jsx)(n.code,{children:"gensubtoken"})," cli command."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"./centrifugo gensubtoken -u 123722 -s channel\n"})}),"\n",(0,t.jsx)(n.p,{children:"You should see an output like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'HMAC SHA-256 JWT for user "123722" and channel "channel" with expiration TTL 168h0m0s:\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM3MjIiLCJleHAiOjE2NTU0NDg0MzgsImNoYW5uZWwiOiJjaGFubmVsIn0.JyRI3ovNV-abV8VxCmZCD556o2F2mNL1UoU58gNR-uI\n'})}),"\n",(0,t.jsx)(n.p,{children:"But in real app subscription JWT must be generated by your application backend."}),"\n",(0,t.jsx)(n.h2,{id:"separate-subscription-token-config",children:"Separate subscription token config"}),"\n",(0,t.jsxs)(n.p,{children:["When ",(0,t.jsx)(n.code,{children:"separate_subscription_token_config"})," boolean option is ",(0,t.jsx)(n.code,{children:"true"})," Centrifugo does not look at general token options at all when verifying subscription tokens and uses config options starting from ",(0,t.jsx)(n.code,{children:"subscription_token_"})," prefix instead."]}),"\n",(0,t.jsx)(n.p,{children:"Here is an example how to use JWKS for connection tokens, but have HMAC-based verification for subscription tokens:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_jwks_public_endpoint": "https://example.com/openid-connect/certs",\n "separate_subscription_token_config": true,\n "subscription_token_hmac_secret_key": "separate_secret_which_must_be_strong"\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All the options which are available for connection token configuration may be re-used for a separate subscription token configuration \u2013 just prefix them with ",(0,t.jsx)(n.code,{children:"subscription_token_"})," instead of ",(0,t.jsx)(n.code,{children:"token_"}),"."]})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},85162:(e,n,i)=>{i.d(n,{Z:()=>o});i(67294);var t=i(36905);const s={tabItem:"tabItem_Ymn6"};var r=i(85893);function o(e){let{children:n,hidden:i,className:o}=e;return(0,r.jsx)("div",{role:"tabpanel",className:(0,t.Z)(s.tabItem,o),hidden:i,children:n})}},74866:(e,n,i)=>{i.d(n,{Z:()=>y});var t=i(67294),s=i(36905),r=i(12466),o=i(16550),a=i(20469),c=i(91980),l=i(67392),d=i(50012);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.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,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:i,attributes:t,default:s}}=e;return{value:n,label:i,attributes:t,default:s}}))}(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 f(e){let{queryString:n=!1,groupId:i}=e;const s=(0,o.k6)(),r=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)(r),(0,t.useCallback)((e=>{if(!r)return;const n=new URLSearchParams(s.location.search);n.set(r,e),s.replace({...s.location,search:n.toString()})}),[r,s])]}function m(e){const{defaultValue:n,queryString:i=!1,groupId:s}=e,r=h(e),[o,c]=(0,t.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 t=i.find((e=>e.default))??i[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:r}))),[l,u]=f({queryString:i,groupId:s}),[m,x]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,r]=(0,d.Nk)(i);return[s,(0,t.useCallback)((e=>{i&&r.set(e)}),[i,r])]}({groupId:s}),b=(()=>{const e=l??m;return p({value:e,tabValues:r})?e:null})();(0,a.Z)((()=>{b&&c(b)}),[b]);return{selectedValue:o,selectValue:(0,t.useCallback)((e=>{if(!p({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);c(e),u(e),x(e)}),[u,x,r]),tabValues:r}}var x=i(72389);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=i(85893);function g(e){let{className:n,block:i,selectedValue:t,selectValue:o,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,r.o5)(),d=e=>{const n=e.currentTarget,i=c.indexOf(n),s=a[i].value;s!==t&&(l(n),o(s))},u=e=>{let n=null;switch(e.key){case"Enter":d(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,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":i},n),children:a.map((e=>{let{value:n,label:i,attributes:r}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:u,onClick:d,...r,className:(0,s.Z)("tabs__item",b.tabItem,r?.className,{"tabs__item--active":t===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:s}=e;const r=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=r.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:r.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function k(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,s.Z)("tabs-container",b.tabList),children:[(0,j.jsx)(g,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function y(e){const n=(0,x.Z)();return(0,j.jsx)(k,{...e,children:u(e.children)},String(n))}},30841:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/subscription_token-77f5e91b2d15d382d3abbef81d78409a.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>a,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 a(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/d2fe6fea.8f81c84d.js b/assets/js/d2fe6fea.8f81c84d.js new file mode 100644 index 000000000..656db6ea5 --- /dev/null +++ b/assets/js/d2fe6fea.8f81c84d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5901],{30433:(e,n,i)=>{i.d(n,{Z:()=>o});i(67294);var t=i(36905);const s={tabItem:"tabItem_Ymn6"};var r=i(85893);function o(e){let{children:n,hidden:i,className:o}=e;return(0,r.jsx)("div",{role:"tabpanel",className:(0,t.Z)(s.tabItem,o),hidden:i,children:n})}},22808:(e,n,i)=>{i.d(n,{Z:()=>y});var t=i(67294),s=i(36905),r=i(63735),o=i(16550),a=i(20613),c=i(34423),l=i(20636),d=i(99200);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.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,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:i,attributes:t,default:s}}=e;return{value:n,label:i,attributes:t,default:s}}))}(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 f(e){let{queryString:n=!1,groupId:i}=e;const s=(0,o.k6)(),r=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)(r),(0,t.useCallback)((e=>{if(!r)return;const n=new URLSearchParams(s.location.search);n.set(r,e),s.replace({...s.location,search:n.toString()})}),[r,s])]}function m(e){const{defaultValue:n,queryString:i=!1,groupId:s}=e,r=h(e),[o,c]=(0,t.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 t=i.find((e=>e.default))??i[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:r}))),[l,u]=f({queryString:i,groupId:s}),[m,x]=function(e){let{groupId:n}=e;const i=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,r]=(0,d.Nk)(i);return[s,(0,t.useCallback)((e=>{i&&r.set(e)}),[i,r])]}({groupId:s}),b=(()=>{const e=l??m;return p({value:e,tabValues:r})?e:null})();(0,a.Z)((()=>{b&&c(b)}),[b]);return{selectedValue:o,selectValue:(0,t.useCallback)((e=>{if(!p({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);c(e),u(e),x(e)}),[u,x,r]),tabValues:r}}var x=i(5730);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=i(85893);function g(e){let{className:n,block:i,selectedValue:t,selectValue:o,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,r.o5)(),d=e=>{const n=e.currentTarget,i=c.indexOf(n),s=a[i].value;s!==t&&(l(n),o(s))},u=e=>{let n=null;switch(e.key){case"Enter":d(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,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":i},n),children:a.map((e=>{let{value:n,label:i,attributes:r}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:u,onClick:d,...r,className:(0,s.Z)("tabs__item",b.tabItem,r?.className,{"tabs__item--active":t===n}),children:i??n},n)}))})}function v(e){let{lazy:n,children:i,selectedValue:s}=e;const r=(Array.isArray(i)?i:[i]).filter(Boolean);if(n){const e=r.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:r.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function k(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,s.Z)("tabs-container",b.tabList),children:[(0,j.jsx)(g,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function y(e){const n=(0,x.Z)();return(0,j.jsx)(k,{...e,children:u(e.children)},String(n))}},75467:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>u});var t=i(85893),s=i(11151),r=i(22808),o=i(30433);const a={id:"channel_token_auth",title:"Channel JWT authorization"},c=void 0,l={id:"server/channel_token_auth",title:"Channel JWT authorization",description:"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.",source:"@site/docs/server/channel_token_auth.md",sourceDirName:"server",slug:"/server/channel_token_auth",permalink:"/docs/server/channel_token_auth",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/server/channel_token_auth.md",tags:[],version:"current",frontMatter:{id:"channel_token_auth",title:"Channel JWT authorization"},sidebar:"Guides",previous:{title:"Channel permission model",permalink:"/docs/server/channel_permissions"},next:{title:"Server-side subscriptions",permalink:"/docs/server/server_subs"}},d={},u=[{value:"Subscription JWT claims",id:"subscription-jwt-claims",level:2},{value:"sub",id:"sub",level:3},{value:"channel",id:"channel",level:3},{value:"info",id:"info",level:3},{value:"b64info",id:"b64info",level:3},{value:"exp",id:"exp",level:3},{value:"expire_at",id:"expire_at",level:3},{value:"aud",id:"aud",level:3},{value:"iss",id:"iss",level:3},{value:"iat",id:"iat",level:3},{value:"jti",id:"jti",level:3},{value:"override",id:"override",level:3},{value:"Example",id:"example",level:2},{value:"gensubtoken cli command",id:"gensubtoken-cli-command",level:2},{value:"Separate subscription token config",id:"separate-subscription-token-config",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",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,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:["In the chapter about ",(0,t.jsx)(n.a,{href:"/docs/server/channel_permissions",children:"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."]}),"\n",(0,t.jsxs)(n.p,{children:["Subscription token is also JWT. Very similar to ",(0,t.jsx)(n.a,{href:"/docs/server/authentication",children:"connection token"}),", but with specific custom claims."]}),"\n",(0,t.jsx)(n.p,{children:"Valid subscription token passed to Centrifugo in subscribe request will tell Centrifugo that subscription must be accepted."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{src:i(30841).Z+"",width:"3062",height:"1173"})}),"\n",(0,t.jsxs)(n.p,{children:["See more info about working with subscription tokens on the client side in ",(0,t.jsx)(n.a,{href:"/docs/transports/client_api#subscription-token",children:"client SDK spec"}),"."]}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Connection token and subscription token are both JWT and both can be generated with any JWT library."})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Even when authorizing a subscription to a channel with a subscription JWT you should still set a proper connection JWT for a client as it provides user authentication details to Centrifugo."})}),"\n",(0,t.jsx)(n.admonition,{type:"tip",children:(0,t.jsx)(n.p,{children:"Just like connection JWT using subscription JWT with a reasonable expiration time may help you have a good level of security in channels and still survive massive reconnect scenario \u2013 when many clients resubscribe alltogether."})}),"\n",(0,t.jsx)(n.p,{children:"Supported JWT algorithms for private subscription tokens match algorithms to create connection JWT. The same HMAC secret key, RSA, and ECDSA public keys set for authentication tokens are re-used to check subscription JWT."}),"\n",(0,t.jsx)(n.h2,{id:"subscription-jwt-claims",children:"Subscription JWT claims"}),"\n",(0,t.jsxs)(n.p,{children:["For subscription JWT Centrifugo uses some standard claims defined in ",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519",children:"rfc7519"}),", also some custom Centrifugo-specific."]}),"\n",(0,t.jsx)(n.h3,{id:"sub",children:"sub"}),"\n",(0,t.jsxs)(n.p,{children:["This is a standard JWT claim which must contain an ID of the current application user (",(0,t.jsx)(n.strong,{children:"as string"}),")."]}),"\n",(0,t.jsx)(n.p,{children:"The value must match a user in connection JWT \u2013 since it's the same real-time connection. The missing claim will mean that token issued for anonymous user (i.e. with empty user ID)."}),"\n",(0,t.jsx)(n.h3,{id:"channel",children:"channel"}),"\n",(0,t.jsxs)(n.p,{children:["Required. Channel that client tries to subscribe to with this token (",(0,t.jsx)(n.strong,{children:"string"}),")."]}),"\n",(0,t.jsx)(n.h3,{id:"info",children:"info"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. Additional information for connection inside this channel (",(0,t.jsx)(n.strong,{children:"valid JSON"}),")."]}),"\n",(0,t.jsx)(n.h3,{id:"b64info",children:"b64info"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. Additional information for connection inside this channel in base64 format (",(0,t.jsx)(n.strong,{children:"string"}),"). Will be decoded by Centrifugo to raw bytes."]}),"\n",(0,t.jsx)(n.h3,{id:"exp",children:"exp"}),"\n",(0,t.jsx)(n.p,{children:"Optional. This is a standard JWT claim that allows setting private channel subscription token expiration time (a UNIX timestamp in the future, in seconds, as integer) and configures subscription expiration time."}),"\n",(0,t.jsxs)(n.p,{children:["At the moment if the subscription expires client connection will be closed and the client will try to reconnect. In most cases, you don't need this and should prefer using the expiration of the connection JWT to deactivate the connection (see ",(0,t.jsx)(n.a,{href:"/docs/server/authentication",children:"authentication"}),"). But if you need more granular per-channel control this may fit your needs."]}),"\n",(0,t.jsxs)(n.p,{children:["Once ",(0,t.jsx)(n.code,{children:"exp"})," is set in token every subscription token must be periodically refreshed. This refresh workflow happens on the client side. Refer to the specific client documentation to see how to refresh subscriptions."]}),"\n",(0,t.jsx)(n.h3,{id:"expire_at",children:"expire_at"}),"\n",(0,t.jsxs)(n.p,{children:["Optional. By default, Centrifugo looks on ",(0,t.jsx)(n.code,{children:"exp"})," claim to both check token expiration and configure subscription expiration time. In most cases this is fine, but there could be situations where you want to decouple subscription token expiration check with subscription expiration time. As soon as the ",(0,t.jsx)(n.code,{children:"expire_at"})," claim is provided (set) in subscription JWT Centrifugo relies on it for setting subscription expiration time (JWT expiration still checked over ",(0,t.jsx)(n.code,{children:"exp"})," though)."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"expire_at"})," is a UNIX timestamp seconds when the subscription should expire."]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Set it to the future time for expiring subscription at some point"}),"\n",(0,t.jsxs)(n.li,{children:["Set it to ",(0,t.jsx)(n.code,{children:"0"})," to disable subscription expiration (but still check token ",(0,t.jsx)(n.code,{children:"exp"})," claim). This allows implementing a one-time subscription token."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"aud",children:"aud"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT audience (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3",children:"rfc7519 aud"})," claim). But if you set ",(0,t.jsx)(n.code,{children:"token_audience"})," option as described in ",(0,t.jsx)(n.a,{href:"/docs/server/authentication#aud",children:"client authentication"})," then audience for subscription JWT will also be checked."]}),"\n",(0,t.jsx)(n.h3,{id:"iss",children:"iss"}),"\n",(0,t.jsxs)(n.p,{children:["By default, Centrifugo does not check JWT issuer (",(0,t.jsx)(n.a,{href:"https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1",children:"rfc7519 iss"})," claim). But if you set ",(0,t.jsx)(n.code,{children:"token_issuer"})," option as described in ",(0,t.jsx)(n.a,{href:"/docs/server/authentication#iss",children:"client authentication"})," then issuer for subscription JWT will also be checked."]}),"\n",(0,t.jsx)(n.h3,{id:"iat",children:"iat"}),"\n",(0,t.jsxs)(n.p,{children:["This is a UNIX time when token was issued (seconds). See ",(0,t.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,t.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"jti",children:"jti"}),"\n",(0,t.jsxs)(n.p,{children:["This is a token unique ID. See ",(0,t.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,t.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"Centrifugo PRO token revocation features"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"override",children:"override"}),"\n",(0,t.jsxs)(n.p,{children:["One more claim is ",(0,t.jsx)(n.code,{children:"override"}),". This is an object which allows overriding channel options for the particular channel subscriber which comes with subscription token."]}),"\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:"Field"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Optional"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"presence"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"presence"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"join_leave"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_push_join_leave"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_push_join_leave"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_recovery"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_recovery"})," channel option"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"force_positioning"}),(0,t.jsx)(n.td,{children:"BoolValue"}),(0,t.jsx)(n.td,{children:"yes"}),(0,t.jsxs)(n.td,{children:["override ",(0,t.jsx)(n.code,{children:"force_positioning"})," channel option"]})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BoolValue"})," is an object like this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "value": true/false\n}\n'})}),"\n",(0,t.jsx)(n.p,{children:"So for example, you want to turn off emitting a presence information for a particular subscriber in a channel:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n ...\n "override": {\n "presence": {\n "value": false\n }\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"example",children:"Example"}),"\n",(0,t.jsxs)(n.p,{children:["So to generate a subscription token you can use something like this in Python (assuming user ID is ",(0,t.jsx)(n.code,{children:"42"})," and the channel is ",(0,t.jsx)(n.code,{children:"gossips"}),"):"]}),"\n","\n","\n",(0,t.jsxs)(r.Z,{className:"unique-tabs",defaultValue:"python",values:[{label:"Python",value:"python"},{label:"NodeJS",value:"node"}],children:[(0,t.jsx)(o.Z,{value:"python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'import jwt\nimport time\n\nclaims = {"sub": "42", "channel": "$gossips", "exp": int(time.time()) + 3600}\ntoken = jwt.encode(claims, "secret", algorithm="HS256").decode()\nprint(token)\n'})})}),(0,t.jsx)(o.Z,{value:"node",children:(0,t.jsx)(n.pre,{children:(0,t.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: '42', channel: '$gossips' })\n .setProtectedHeader({ alg })\n .setExpirationTime('1h')\n .sign(secret)\n\n console.log(token);\n})();\n"})})})]}),"\n",(0,t.jsxs)(n.p,{children:["Where ",(0,t.jsx)(n.code,{children:'"secret"'})," is the ",(0,t.jsx)(n.code,{children:"token_hmac_secret_key"})," from Centrifugo configuration (we use HMAC tokens in this example which relies on a shared secret key, for RSA or ECDSA tokens you need to use a private key known only by your backend)."]}),"\n",(0,t.jsx)(n.h2,{id:"gensubtoken-cli-command",children:"gensubtoken cli command"}),"\n",(0,t.jsxs)(n.p,{children:["During development you can quickly generate valid subscription token using Centrifugo ",(0,t.jsx)(n.code,{children:"gensubtoken"})," cli command."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"./centrifugo gensubtoken -u 123722 -s channel\n"})}),"\n",(0,t.jsx)(n.p,{children:"You should see an output like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'HMAC SHA-256 JWT for user "123722" and channel "channel" with expiration TTL 168h0m0s:\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM3MjIiLCJleHAiOjE2NTU0NDg0MzgsImNoYW5uZWwiOiJjaGFubmVsIn0.JyRI3ovNV-abV8VxCmZCD556o2F2mNL1UoU58gNR-uI\n'})}),"\n",(0,t.jsx)(n.p,{children:"But in real app subscription JWT must be generated by your application backend."}),"\n",(0,t.jsx)(n.h2,{id:"separate-subscription-token-config",children:"Separate subscription token config"}),"\n",(0,t.jsxs)(n.p,{children:["When ",(0,t.jsx)(n.code,{children:"separate_subscription_token_config"})," boolean option is ",(0,t.jsx)(n.code,{children:"true"})," Centrifugo does not look at general token options at all when verifying subscription tokens and uses config options starting from ",(0,t.jsx)(n.code,{children:"subscription_token_"})," prefix instead."]}),"\n",(0,t.jsx)(n.p,{children:"Here is an example how to use JWKS for connection tokens, but have HMAC-based verification for subscription tokens:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",metastring:'title="config.json"',children:'{\n "token_jwks_public_endpoint": "https://example.com/openid-connect/certs",\n "separate_subscription_token_config": true,\n "subscription_token_hmac_secret_key": "separate_secret_which_must_be_strong"\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["All the options which are available for connection token configuration may be re-used for a separate subscription token configuration \u2013 just prefix them with ",(0,t.jsx)(n.code,{children:"subscription_token_"})," instead of ",(0,t.jsx)(n.code,{children:"token_"}),"."]})]})}function p(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},30841:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/subscription_token-77f5e91b2d15d382d3abbef81d78409a.png"},11151:(e,n,i)=>{i.d(n,{Z:()=>a,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 a(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/ee78c395.06d3c57d.js b/assets/js/ee78c395.06d3c57d.js deleted file mode 100644 index 60ffd65c2..000000000 --- a/assets/js/ee78c395.06d3c57d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5484],{88341:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>t,metadata:()=>a,toc:()=>c});var r=i(85893),o=i(11151);const t={id:"overview",title:"Centrifugo PRO"},s=void 0,a={id:"pro/overview",title:"Centrifugo PRO",description:"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It's packed with a set of unique features that offer exceptional benefits to your business. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), and more.",source:"@site/docs/pro/overview.md",sourceDirName:"pro",slug:"/pro/overview",permalink:"/docs/pro/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/overview.md",tags:[],version:"current",frontMatter:{id:"overview",title:"Centrifugo PRO"},sidebar:"Pro",next:{title:"Install and run PRO version",permalink:"/docs/pro/install_and_run"}},l={},c=[{value:"Features",id:"features",level:2},{value:"Pricing",id:"pricing",level:2},{value:"Try for free in sandbox mode",id:"try-for-free-in-sandbox-mode",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",ul:"ul",...(0,o.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("img",{src:"/img/pro_icon.png",width:"110px",height:"110px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It's packed with a set of unique features that offer exceptional benefits to your business. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), and more."}),"\n",(0,r.jsx)(n.p,{children:"All the features of Centrifugo PRO come with a decent scalable performance. Some reuse Centrifugo super fast Redis communication capabilities. ClickHouse analytics built on top of efficient approach with the minimal overhead. We've put a lot of love into all of the extra powers of Centrifugo to make sure they are practical and ready for production workloads."}),"\n",(0,r.jsx)(n.h2,{id:"features",children:"Features"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo PRO is packed with the following features:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Everything from Centrifugo OSS"}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd0d ",(0,r.jsx)(n.a,{href:"/docs/pro/tracing",children:"Channel and user tracing"})," allows watching client protocol frames in channel or per user ID in real time."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcb9 ",(0,r.jsx)(n.a,{href:"/docs/pro/analytics",children:"Real-time analytics with ClickHouse"})," for a great system observability, reporting and trending."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udee1\ufe0f ",(0,r.jsx)(n.a,{href:"/docs/pro/rate_limiting",children:"Operation rate limits"})," to protect server from the real-time API misusing and frontend bugs."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd25 ",(0,r.jsx)(n.a,{href:"/docs/pro/push_notifications",children:"Push notification API"})," to manage device tokens and send mobile and browser push notifications."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udfe2 ",(0,r.jsx)(n.a,{href:"/docs/pro/user_status",children:"User status API"})," feature allows understanding activity state for a list of users."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd0c ",(0,r.jsx)(n.a,{href:"/docs/pro/connections",children:"Connections API"})," to query, filter and inspect active connections."]}),"\n",(0,r.jsxs)(n.li,{children:["\u270b ",(0,r.jsx)(n.a,{href:"/docs/pro/user_block",children:"User blocking API"})," to block/unblock abusive users by ID."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\uded1 ",(0,r.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"JWT revoking and invalidation API"})," to revoke tokens by ID and invalidate user's tokens based on issue time."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd14 ",(0,r.jsx)(n.a,{href:"/docs/pro/channel_state_events",children:"Channel state events"})," to be notified on the backend about channel ",(0,r.jsx)(n.code,{children:"occupied"})," and ",(0,r.jsx)(n.code,{children:"vacated"})," events."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcaa ",(0,r.jsx)(n.a,{href:"/docs/pro/capabilities",children:"Channel capabilities"})," for controlling channel permissions per connection or per subscription."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcdc ",(0,r.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"Channel patterns"})," allow defining channel configuration like HTTP routes with parameters."]}),"\n",(0,r.jsxs)(n.li,{children:["\u270d\ufe0f ",(0,r.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"CEL expressions"})," to write custom efficient permission rules for channel operations."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\ude80 ",(0,r.jsx)(n.a,{href:"/docs/pro/performance",children:"Faster performance"})," to reduce resource usage on server side."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd2e ",(0,r.jsx)(n.a,{href:"/docs/pro/singleflight",children:"Singleflight"})," for online presence and history to reduce load on the broker."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83c\udf54 ",(0,r.jsx)(n.a,{href:"/docs/pro/client_message_batching",children:"Message batching control"})," for advanced tuning of client connection write behaviour."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83e\uddd0 ",(0,r.jsx)(n.a,{href:"/docs/pro/observability_enhancements",children:"Observability enhancements"})," for additional more granular system state insights."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83e\udeb5 ",(0,r.jsx)(n.a,{href:"/docs/pro/process_stats",children:"CPU and RSS memory"})," usage stats of Centrifugo nodes in admin UI."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Also, explore our ",(0,r.jsx)(n.a,{href:"https://github.com/orgs/centrifugal/projects/3/views/1",children:"Centrifugo PRO planned features"})," board for a concise overview of upcoming features which are currently in progress and enhancements planned for a future."]}),"\n",(0,r.jsx)(n.h2,{id:"pricing",children:"Pricing"}),"\n",(0,r.jsxs)(n.p,{children:["Centrifugo PRO requires a license key to run. The pricing information for the license key is available upon request over ",(0,r.jsx)(n.code,{children:"sales@centrifugal.dev"})," e-mail. Our services are exclusively available to corporate and business clients at this time. We would be happy to learn more about your real-time challenges and how Centrifugo can help you address them. Don't hesitate to ask for an online meeting to discuss the use case in-person."]}),"\n",(0,r.jsx)(n.h2,{id:"try-for-free-in-sandbox-mode",children:"Try for free in sandbox mode"}),"\n",(0,r.jsx)(n.p,{children:"You can try out Centrifugo PRO for free. When you start Centrifugo PRO without license key then it's running in a sandbox mode. Sandbox mode limits the usage of Centrifigo PRO in several ways. For example:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Centrifugo handles up to 20 concurrent connections"}),"\n",(0,r.jsx)(n.li,{children:"up to 2 server nodes supported"}),"\n",(0,r.jsx)(n.li,{children:"up to 5 API requests per second allowed"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"This mode should be enough for development and trying out PRO features, but must not be used in production environment as we can introduce additional limitations in the future."}),"\n",(0,r.jsx)(n.admonition,{title:"Centrifugo PRO license agreement",type:"caution",children:(0,r.jsxs)(n.p,{children:["Centrifugo PRO is distributed by Centrifugal Labs LTD under ",(0,r.jsx)(n.a,{href:"/license",children:"commercial license"})," which is different from OSS version. By downloading Centrifugo PRO you automatically accept commercial license terms."]})})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},11151:(e,n,i)=>{i.d(n,{Z:()=>a,a:()=>s});var r=i(67294);const o={},t=r.createContext(o);function s(e){const n=r.useContext(t);return r.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(o):e.components||o:s(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/ee78c395.e5aeb3e1.js b/assets/js/ee78c395.e5aeb3e1.js new file mode 100644 index 000000000..757de784f --- /dev/null +++ b/assets/js/ee78c395.e5aeb3e1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[5484],{88341:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>t,metadata:()=>a,toc:()=>c});var r=i(85893),o=i(11151);const t={id:"overview",title:"Centrifugo PRO"},s=void 0,a={id:"pro/overview",title:"Centrifugo PRO",description:"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It's packed with a set of unique features offering exceptional benefits to corporate and enterprise environments. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), SSO integrations for admin UI, and more.",source:"@site/docs/pro/overview.md",sourceDirName:"pro",slug:"/pro/overview",permalink:"/docs/pro/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/centrifugal/centrifugal.dev/edit/main/docs/pro/overview.md",tags:[],version:"current",frontMatter:{id:"overview",title:"Centrifugo PRO"},sidebar:"Pro",next:{title:"Install and run PRO version",permalink:"/docs/pro/install_and_run"}},l={},c=[{value:"Features",id:"features",level:2},{value:"Pricing",id:"pricing",level:2},{value:"Try for free in sandbox mode",id:"try-for-free-in-sandbox-mode",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",p:"p",ul:"ul",...(0,o.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("img",{src:"/img/pro_icon.png",width:"110px",height:"110px",align:"left",style:{marginRight:"10px",float:"left"}}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo PRO is the enhanced version of Centrifugo provided by Centrifugal Labs LTD under commercial license. It's packed with a set of unique features offering exceptional benefits to corporate and enterprise environments. It provides granular channel permission control, lower CPU utilization on Centrifugo nodes, backend protection from misusing, next level system observability, additional APIs (like push notifications), SSO integrations for admin UI, and more."}),"\n",(0,r.jsx)(n.p,{children:"All the features of Centrifugo PRO come with a decent scalable performance. Some reuse Centrifugo super fast Redis communication capabilities. ClickHouse analytics built on top of efficient approach with the minimal overhead. We've put a lot of love into all of the extra powers of Centrifugo to make sure they are practical and ready for production workloads."}),"\n",(0,r.jsx)(n.h2,{id:"features",children:"Features"}),"\n",(0,r.jsx)(n.p,{children:"Centrifugo PRO is packed with the following features:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Everything from Centrifugo OSS"}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd0d ",(0,r.jsx)(n.a,{href:"/docs/pro/tracing",children:"Channel and user tracing"})," allows watching client protocol frames in channel or per user ID in real time."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcb9 ",(0,r.jsx)(n.a,{href:"/docs/pro/analytics",children:"Real-time analytics with ClickHouse"})," for a great system observability, reporting and trending."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udee1\ufe0f ",(0,r.jsx)(n.a,{href:"/docs/pro/rate_limiting",children:"Operation rate limits"})," to protect server from the real-time API misusing and frontend bugs."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd25 ",(0,r.jsx)(n.a,{href:"/docs/pro/push_notifications",children:"Push notification API"})," to manage device tokens and send mobile and browser push notifications."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd10 ",(0,r.jsx)(n.a,{href:"/docs/pro/admin_idp_auth",children:"SSO for admin UI"})," using OpenID Connect (OIDC) protocol."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udfe2 ",(0,r.jsx)(n.a,{href:"/docs/pro/user_status",children:"User status API"})," feature allows understanding activity state for a list of users."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd0c ",(0,r.jsx)(n.a,{href:"/docs/pro/connections",children:"Connections API"})," to query, filter and inspect active connections."]}),"\n",(0,r.jsxs)(n.li,{children:["\u270b ",(0,r.jsx)(n.a,{href:"/docs/pro/user_block",children:"User blocking API"})," to block/unblock abusive users by ID."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\uded1 ",(0,r.jsx)(n.a,{href:"/docs/pro/token_revocation",children:"JWT revoking and invalidation API"})," to revoke tokens by ID and invalidate user's tokens based on issue time."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd14 ",(0,r.jsx)(n.a,{href:"/docs/pro/channel_state_events",children:"Channel state events"})," to be notified on the backend about channel ",(0,r.jsx)(n.code,{children:"occupied"})," and ",(0,r.jsx)(n.code,{children:"vacated"})," events."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcaa ",(0,r.jsx)(n.a,{href:"/docs/pro/capabilities",children:"Channel capabilities"})," for controlling channel permissions per connection or per subscription."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udcdc ",(0,r.jsx)(n.a,{href:"/docs/pro/channel_patterns",children:"Channel patterns"})," allow defining channel configuration like HTTP routes with parameters."]}),"\n",(0,r.jsxs)(n.li,{children:["\u270d\ufe0f ",(0,r.jsx)(n.a,{href:"/docs/pro/cel_expressions",children:"Channel CEL expressions"})," to write custom efficient permission rules for channel operations."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\ude80 ",(0,r.jsx)(n.a,{href:"/docs/pro/performance",children:"Faster performance"})," to reduce resource usage on server side."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83d\udd2e ",(0,r.jsx)(n.a,{href:"/docs/pro/singleflight",children:"Singleflight"})," for online presence and history to reduce load on the broker."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83c\udf54 ",(0,r.jsx)(n.a,{href:"/docs/pro/client_message_batching",children:"Message batching control"})," for advanced tuning of client connection write behaviour."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83e\uddd0 ",(0,r.jsx)(n.a,{href:"/docs/pro/observability_enhancements",children:"Observability enhancements"})," for additional more granular system state insights."]}),"\n",(0,r.jsxs)(n.li,{children:["\ud83e\udeb5 ",(0,r.jsx)(n.a,{href:"/docs/pro/process_stats",children:"CPU and RSS memory"})," usage stats of Centrifugo nodes in admin UI."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Also, explore our ",(0,r.jsx)(n.a,{href:"https://github.com/orgs/centrifugal/projects/3/views/1",children:"Centrifugo PRO planned features"})," board for a concise overview of upcoming features which are currently in progress and enhancements planned for a future."]}),"\n",(0,r.jsx)(n.h2,{id:"pricing",children:"Pricing"}),"\n",(0,r.jsxs)(n.p,{children:["Centrifugo PRO requires a license key to run. The pricing information for the license key is available upon request over ",(0,r.jsx)(n.code,{children:"sales@centrifugal.dev"})," e-mail. Our services are exclusively available to corporate and business clients at this time. We would be happy to learn more about your real-time challenges and how Centrifugo can help you address them. Don't hesitate to ask for an online meeting to discuss the use case in-person."]}),"\n",(0,r.jsx)(n.h2,{id:"try-for-free-in-sandbox-mode",children:"Try for free in sandbox mode"}),"\n",(0,r.jsx)(n.p,{children:"You can try out Centrifugo PRO for free. When you start Centrifugo PRO without license key then it's running in a sandbox mode. Sandbox mode limits the usage of Centrifigo PRO in several ways. For example:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Centrifugo handles up to 20 concurrent connections"}),"\n",(0,r.jsx)(n.li,{children:"up to 2 server nodes supported"}),"\n",(0,r.jsx)(n.li,{children:"up to 5 API requests per second allowed"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"This mode should be enough for development and trying out PRO features, but must not be used in production environment as we can introduce additional limitations in the future."}),"\n",(0,r.jsx)(n.admonition,{title:"Centrifugo PRO license agreement",type:"caution",children:(0,r.jsxs)(n.p,{children:["Centrifugo PRO is distributed by Centrifugal Labs LTD under ",(0,r.jsx)(n.a,{href:"/license",children:"commercial license"})," which is different from OSS version. By downloading Centrifugo PRO you automatically accept commercial license terms."]})})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},11151:(e,n,i)=>{i.d(n,{Z:()=>a,a:()=>s});var r=i(67294);const o={},t=r.createContext(o);function s(e){const n=r.useContext(t);return r.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(o):e.components||o:s(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/fd93cfee.c7d91946.js b/assets/js/fd93cfee.e8a808c3.js similarity index 95% rename from assets/js/fd93cfee.c7d91946.js rename to assets/js/fd93cfee.e8a808c3.js index 0547cd773..96d32c989 100644 --- a/assets/js/fd93cfee.c7d91946.js +++ b/assets/js/fd93cfee.e8a808c3.js @@ -1 +1 @@ -"use strict";(self.webpackChunkcentrifugal_dev=self.webpackChunkcentrifugal_dev||[]).push([[9217],{59250:(e,t,r)=>{r.r(t),r.d(t,{default:()=>i});r(67294);var a=r(86010);const n={featureTitle:"featureTitle_fCfN",featureContent:"featureContent_FT24",featureContentReversed:"featureContentReversed_juV3",featureImage:"featureImage_lJJP",darkSection:"darkSection_dAst",featureImageReversed:"featureImageReversed_n2Vd"};var s=r(85893);function i(e){let{reversed:t,title:r,img:i,text:c,isDark:f}=e;const l=(0,s.jsx)("div",{className:(0,a.Z)("col col--6",n.featureImage,t?n.featureImageReversed:""),children:i}),o=(0,s.jsxs)("div",{className:(0,a.Z)("col col--6",n.featureContent,t?n.featureContentReversed:""),children:[(0,s.jsx)("h3",{className:n.featureTitle,children:r}),c]});return(0,s.jsx)("section",{className:(0,a.Z)("highlightSection",f?n.darkSection+" darkSection":""),children:(0,s.jsx)("div",{className:"container",children:(0,s.jsx)("div",{className:"row",children:t?(0,s.jsxs)(s.Fragment,{children:[o,l]}):(0,s.jsxs)(s.Fragment,{children:[l,o]})})})})}},86010:(e,t,r)=>{function a(e){var t,r,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;t n});const n=function(){for(var e,t,r=0,n="";r {r.r(t),r.d(t,{default:()=>i});r(67294);var a=r(86010);const n={featureTitle:"featureTitle_fCfN",featureContent:"featureContent_FT24",featureContentReversed:"featureContentReversed_juV3",featureImage:"featureImage_lJJP",darkSection:"darkSection_dAst",featureImageReversed:"featureImageReversed_n2Vd"};var s=r(85893);function i(e){let{reversed:t,title:r,img:i,text:c,isDark:f}=e;const l=(0,s.jsx)("div",{className:(0,a.Z)("col col--6",n.featureImage,t?n.featureImageReversed:""),children:i}),o=(0,s.jsxs)("div",{className:(0,a.Z)("col col--6",n.featureContent,t?n.featureContentReversed:""),children:[(0,s.jsx)("h3",{className:n.featureTitle,children:r}),c]});return(0,s.jsx)("section",{className:(0,a.Z)("highlightSection",f?n.darkSection+" darkSection":""),children:(0,s.jsx)("div",{className:"container",children:(0,s.jsx)("div",{className:"row",children:t?(0,s.jsxs)(s.Fragment,{children:[o,l]}):(0,s.jsxs)(s.Fragment,{children:[l,o]})})})})}},86010:(e,t,r)=>{function a(e){var t,r,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;t n});const n=function(){for(var e,t,r=0,n="";r {"use strict";n.d(t,{W:()=>o});var r=n(67294);function o(){return r.createElement("svg",{width:"20",height:"20",className:"DocSearch-Search-Icon",viewBox:"0 0 20 20"},r.createElement("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}},21204:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});n(67294);var r=n(68356),o=n.n(r),a=n(16887);const s={"0136d6f0":[()=>n.e(1152).then(n.bind(n,9838)),"@site/versioned_docs/version-3/getting-started/introduction.md",9838],"01a85c17":[()=>Promise.all([n.e(3312),n.e(4013)]).then(n.bind(n,54057)),"@theme/BlogTagsListPage",54057],"01eca2db":[()=>n.e(7717).then(n.bind(n,46499)),"@site/blog/2021-12-14-laravel-multi-room-chat-tutorial.md",46499],"02080b57":[()=>n.e(9040).then(n.t.bind(n,94569,19)),"~blog/default/blog-tags-centrifugo-310-list.json",94569],"0295581e":[()=>n.e(6709).then(n.bind(n,70736)),"@site/versioned_docs/version-4/server/codes.md",70736],"02c21cfa":[()=>n.e(8695).then(n.bind(n,36392)),"@site/docs/tutorial/outbox_cdc.md",36392],"0312644c":[()=>n.e(4841).then(n.t.bind(n,21915,19)),"~blog/default/blog-tags-push-notifications-7de.json",21915],"03e522d9":[()=>n.e(2624).then(n.bind(n,50194)),"@site/docs/pro/token_revocation.md",50194],"041d543b":[()=>n.e(3784).then(n.bind(n,67747)),"@site/versioned_docs/version-3/server/tls.md",67747],"04ac3258":[()=>n.e(8356).then(n.bind(n,39267)),"@site/docs/pro/tracing.md",39267],"05ba4f60":[()=>n.e(7459).then(n.t.bind(n,75554,19)),"~blog/default/blog-tags-proxy-b6a.json",75554],"06f11616":[()=>n.e(3255).then(n.bind(n,33298)),"@site/blog/2021-01-15-centrifuge-intro.md?truncated=true",33298],"06f9ead7":[()=>n.e(8896).then(n.bind(n,31929)),"@site/docs/server/channels.md",31929],"07703c67":[()=>n.e(1658).then(n.bind(n,21469)),"@site/docs/server/history_and_recovery.md",21469],"09382599":[()=>n.e(8951).then(n.bind(n,73857)),"@site/src/pages/components/logos/Badoo.js",73857],"0a1a814f":[()=>n.e(2321).then(n.t.bind(n,70193,19)),"~blog/default/blog-tags-laravel-bcc.json",70193],"0a4443e6":[()=>n.e(628).then(n.bind(n,90793)),"@site/docs/server/monitoring.md",90793],"0a7ca2d6":[()=>n.e(4691).then(n.bind(n,90302)),"@site/versioned_docs/version-3/pro/performance.md",90302],"0d0ff016":[()=>n.e(1616).then(n.bind(n,89051)),"@site/docs/server/load_balancing.md",89051],"0d503bfe":[()=>n.e(7401).then(n.bind(n,82923)),"@site/docs/pro/admin_idp_auth.md",82923],"0d57d15e":[()=>n.e(8246).then(n.bind(n,39065)),"@site/versioned_docs/version-3/getting-started/integration.md",39065],"0dc36dc4":[()=>n.e(6990).then(n.bind(n,15594)),"@site/versioned_docs/version-4/getting-started/introduction.md",15594],"0eae5577":[()=>Promise.all([n.e(3312),n.e(5048)]).then(n.bind(n,41183)),"@site/blog/2022-07-29-101-way-to-subscribe.md",41183],"0fe76b3c":[()=>n.e(992).then(n.bind(n,52057)),"@site/versioned_docs/version-3/pro/singleflight.md",52057],"1117f49a":[()=>n.e(4787).then(n.bind(n,12132)),"@site/docs/getting-started/community.md",12132],"11a20880":[()=>n.e(7422).then(n.bind(n,77018)),"@site/versioned_docs/version-4/transports/uni_sse.md",77018],"1248e41e":[()=>n.e(3672).then(n.bind(n,26314)),"@site/docs/pro/analytics.md",26314],"129cb555":[()=>n.e(1875).then(n.t.bind(n,7085,19)),"/Users/fz/projects/centrifugal/centrifugal.dev/.docusaurus/docusaurus-theme-search-algolia/default/plugin-route-context-module-100.json",7085],"1335c7a1":[()=>n.e(5343).then(n.bind(n,16475)),"@site/versioned_docs/version-4/pro/cel_expressions.md",16475],"13f6d888":[()=>n.e(5270).then(n.bind(n,69131)),"@site/versioned_docs/version-3/server/monitoring.md",69131],"155d95c4":[()=>n.e(9443).then(n.bind(n,60052)),"@site/versioned_docs/version-3/transports/sockjs.md",60052],"16c9b55b":[()=>n.e(5432).then(n.bind(n,41326)),"@site/docs/server/codes.md",41326],17896441:[()=>Promise.all([n.e(3312),n.e(1343),n.e(7264),n.e(7918)]).then(n.bind(n,47488)),"@theme/DocItem",47488],18793598:[()=>n.e(2278).then(n.bind(n,83951)),"@site/src/pages/components/logo.js",83951],"18b1e3cf":[()=>n.e(7417).then(n.bind(n,48356)),"@site/versioned_docs/version-4/server/server_api.md",48356],"192a8b1e":[()=>n.e(5069).then(n.bind(n,47262)),"@site/docs/server/engines.md",47262],"195b633a":[()=>n.e(9571).then(n.bind(n,36591)),"@site/docs/pro/client_msg_batching.md",36591],"19e7756f":[()=>Promise.all([n.e(3312),n.e(1343),n.e(7810)]).then(n.bind(n,75896)),"@site/src/pages/license_exchange_lemon.md",75896],"1a4e3797":[()=>Promise.all([n.e(3312),n.e(7920)]).then(n.bind(n,19291)),"@theme/SearchPage",19291],"1cd70467":[()=>n.e(6533).then(n.bind(n,82191)),"@site/blog/2020-02-10-million-connections-with-centrifugo.md",82191],"1d223b96":[()=>n.e(9934).then(n.t.bind(n,58299,19)),"~blog/default/blog-tags-quic-5d2.json",58299],"1d3c9151":[()=>n.e(3981).then(n.bind(n,20999)),"@site/versioned_docs/version-4/pro/singleflight.md",20999],"1d4d4d48":[()=>n.e(9727).then(n.bind(n,21363)),"@site/versioned_docs/version-3/server/proxy.md",21363],"1f391b9e":[()=>Promise.all([n.e(3312),n.e(1343),n.e(7264),n.e(3085)]).then(n.bind(n,29688)),"@theme/MDXPage",29688],"1fadf4c6":[()=>n.e(1633).then(n.t.bind(n,97424,19)),"~blog/default/blog-tags-django-c32.json",97424],"209857dd":[()=>n.e(9804).then(n.bind(n,19970)),"@site/versioned_docs/version-4/server/channel_permissions.md",19970],"20c4d804":[()=>n.e(7973).then(n.bind(n,94567)),"@site/docs/tutorial/scale.md",94567],"2114e513":[()=>n.e(1313).then(n.bind(n,27276)),"@site/versioned_docs/version-3/server/private_channels.md",27276],"211f1d7a":[()=>Promise.all([n.e(3312),n.e(2540)]).then(n.bind(n,70733)),"@site/versioned_docs/version-3/server/authentication.md",70733],"21c2d27e":[()=>n.e(3205).then(n.bind(n,86655)),"@site/versioned_docs/version-4/getting-started/installation.md",86655],"21e8a749":[()=>n.e(2026).then(n.bind(n,28652)),"@site/versioned_docs/version-3/pro/overview.md",28652],"230ca58d":[()=>n.e(4085).then(n.bind(n,81657)),"@site/docs/pro/performance.md",81657],"237c01c3":[()=>n.e(4295).then(n.bind(n,58362)),"@site/versioned_docs/version-4/attributions.md",58362],"23855fe2":[()=>n.e(8).then(n.t.bind(n,40912,19)),"~blog/default/blog-tags-redis-216.json",40912],"238ce909":[()=>n.e(4131).then(n.bind(n,9272)),"@site/blog/2023-06-29-centrifugo-v5-released.md?truncated=true",9272],"2391cf3d":[()=>n.e(9523).then(n.bind(n,66796)),"@site/src/pages/components/logos/ManyChat.js",66796],"24f2bde3":[()=>n.e(2639).then(n.t.bind(n,25348,19)),"~blog/default/blog-tags-interview-9a2-list.json",25348],"250ad80d":[()=>n.e(9309).then(n.bind(n,489)),"@site/versioned_docs/version-3/pro/user_block.md",489],"25c94216":[()=>n.e(3343).then(n.bind(n,45198)),"@site/versioned_docs/version-3/transports/uni_http_stream.md",45198],"267a22d2":[()=>n.e(9668).then(n.bind(n,8441)),"@site/versioned_docs/version-4/pro/user_status.md",8441],"267fbc4e":[()=>n.e(6686).then(n.bind(n,42929)),"@site/versioned_docs/version-4/pro/overview.md",42929],29839967:[()=>n.e(2291).then(n.bind(n,44104)),"@site/versioned_docs/version-3/transports/uni_grpc.md",44104],"2a42cb18":[()=>n.e(7115).then(n.bind(n,59453)),"@site/docs/getting-started/migration-v4.md",59453],"2acc7b89":[()=>n.e(9226).then(n.bind(n,35007)),"@site/versioned_docs/version-3/ecosystem/centrifuge.md",35007],"2b147458":[()=>n.e(2757).then(n.bind(n,42859)),"@site/docs/server/admin_web.md",42859],"2b8ad662":[()=>n.e(4035).then(n.bind(n,85335)),"@site/docs/transports/uni_client_protocol.md",85335],"2bf25d27":[()=>n.e(7815).then(n.bind(n,98082)),"@site/versioned_docs/version-4/server/console_commands.md",98082],"2daf4703":[()=>n.e(6791).then(n.bind(n,40998)),"@site/docs/transports/uni_sse.md",40998],"2dbf7ee0":[()=>n.e(3581).then(n.bind(n,86064)),"@site/docs/server/presence.md",86064],"2e854b47":[()=>n.e(7287).then(n.bind(n,69608)),"@site/docs/tutorial/intro.md",69608],"2eb9c429":[()=>n.e(4968).then(n.bind(n,39012)),"@site/src/pages/components/logos/OpenWeb.js",39012],"2f70c421":[()=>Promise.all([n.e(3312),n.e(4633)]).then(n.bind(n,75282)),"@site/versioned_docs/version-4/transports/client_api.md",75282],"30b3ad4a":[()=>n.e(5304).then(n.bind(n,22595)),"@site/versioned_docs/version-3/server/codes.md",22595],"31c58a66":[()=>n.e(8072).then(n.bind(n,93399)),"@site/versioned_docs/version-4/getting-started/ecosystem.md",93399],"32663f72":[()=>n.e(5731).then(n.bind(n,56718)),"@site/docs/tutorial/backend.md",56718],"3270d7e8":[()=>n.e(7339).then(n.t.bind(n,13503,19)),"~blog/default/blog-tags-benthos-fe1.json",13503],"32e1c903":[()=>n.e(5990).then(n.bind(n,171)),"@site/versioned_docs/version-4/transports/uni_http_stream.md",171],"332362b2":[()=>n.e(2918).then(n.bind(n,75215)),"@site/docs/server/configuration.md",75215],"3529cd5b":[()=>n.e(2983).then(n.bind(n,57280)),"@site/versioned_docs/version-3/server/history_and_recovery.md",57280],"3630fee7":[()=>n.e(216).then(n.bind(n,10526)),"@site/blog/2020-10-16-experimenting-with-quic-transport.md?truncated=true",10526],"369aea06":[()=>n.e(5859).then(n.bind(n,99305)),"@site/versioned_docs/version-4/pro/tracing.md",99305],"369bd8f8":[()=>n.e(5391).then(n.t.bind(n,75650,19)),"~blog/default/blog-tags-tutorial-e7d-list.json",75650],"386a3726":[()=>n.e(9093).then(n.bind(n,24265)),"@site/versioned_docs/version-4/transports/uni_grpc.md",24265],"39d4d18a":[()=>n.e(5625).then(n.bind(n,52372)),"@site/versioned_docs/version-3/getting-started/migration-v3.md",52372],"3a890c2d":[()=>n.e(1385).then(n.bind(n,48477)),"@site/versioned_docs/version-3/transports/uni_sse.md",48477],"3b028b51":[()=>n.e(2292).then(n.bind(n,15746)),"@site/versioned_docs/version-4/transports/sockjs.md",15746],"3bb37b67":[()=>n.e(2413).then(n.bind(n,40358)),"@site/versioned_docs/version-3/getting-started/installation.md",40358],"3c4ec49c":[()=>n.e(5358).then(n.bind(n,42462)),"@site/docs/getting-started/highlights.md",42462],"3c51ccb2":[()=>n.e(4160).then(n.bind(n,82639)),"@site/versioned_docs/version-4/pro/push_notifications.md",82639],"3d15ab27":[()=>n.e(3734).then(n.bind(n,74881)),"@site/docs/tutorial/recovery.md",74881],"3dfd29d6":[()=>n.e(2005).then(n.bind(n,28408)),"@site/versioned_docs/version-4/getting-started/community.md",28408],"3e15fc9c":[()=>n.e(4073).then(n.bind(n,98672)),"@site/versioned_docs/version-4/transports/client_sdk.md",98672],"3f0e28d9":[()=>n.e(6253).then(n.bind(n,42440)),"@site/versioned_docs/version-4/pro/channel_patterns.md",42440],"40537b69":[()=>n.e(683).then(n.bind(n,8322)),"@site/versioned_docs/version-3/pro/tracing.md",8322],41068141:[()=>n.e(3812).then(n.t.bind(n,23690,19)),"~blog/default/blog-tags-pro-ddd.json",23690],"4268d52f":[()=>n.e(895).then(n.bind(n,84096)),"@site/versioned_docs/version-4/server/proxy.md",84096],"43c444d2":[()=>n.e(2853).then(n.t.bind(n,46129,19)),"~blog/default/blog-tags-webtransport-009-list.json",46129],"4529506a":[()=>n.e(7272).then(n.bind(n,49829)),"@site/docs/tutorial/centrifugo.md",49829],"45dfef24":[()=>n.e(1714).then(n.bind(n,35621)),"@site/versioned_docs/version-4/pro/token_revocation.md",35621],"46627d28":[()=>n.e(2203).then(n.bind(n,49322)),"@site/blog/2023-08-29-using-centrifugo-in-rabbitx.md?truncated=true",49322],"49012ebf":[()=>n.e(9604).then(n.bind(n,19036)),"@site/versioned_docs/version-4/pro/analytics.md",19036],"498554e3":[()=>n.e(9476).then(n.bind(n,85902)),"@site/versioned_docs/version-3/transports/uni_websocket.md",85902],"4a4109ec":[()=>n.e(7965).then(n.bind(n,85348)),"@site/docs/server/channel_permissions.md",85348],"4ab79476":[()=>n.e(1428).then(n.t.bind(n,83769,19)),"/Users/fz/projects/centrifugal/centrifugal.dev/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",83769],"4bd4488a":[()=>n.e(6484).then(n.bind(n,53103)),"@site/docs/flow_diagrams.md",53103],"4ebb2955":[()=>n.e(3190).then(n.bind(n,90368)),"@site/blog/2022-12-20-improving-redis-engine-performance.md",90368],"4ec37bcb":[()=>n.e(8899).then(n.bind(n,36549)),"@site/docs/tutorial/layout.md",36549],"4efbf0bc":[()=>n.e(9201).then(n.bind(n,91923)),"@site/versioned_docs/version-4/pro/capabilities.md",91923],"4f64b982":[()=>n.e(3895).then(n.bind(n,665)),"@site/versioned_docs/version-3/pro/token_revocation.md",665],"4fc58f03":[()=>n.e(8238).then(n.bind(n,8626)),"@site/docs/transports/uni_grpc.md",8626],"52bba951":[()=>n.e(102).then(n.bind(n,24203)),"@site/versioned_docs/version-4/transports/sse.md",24203],"54aee988":[()=>n.e(347).then(n.bind(n,84777)),"@site/versioned_docs/version-4/transports/http_stream.md",84777],"54f44165":[()=>n.e(152).then(n.bind(n,59145)),"@site/docs/getting-started/installation.md",59145],"555afbe5":[()=>n.e(2925).then(n.bind(n,23590)),"@site/docs/pro/singleflight.md",23590],56231886:[()=>n.e(5407).then(n.bind(n,34037)),"@site/versioned_docs/version-3/getting-started/quickstart.md",34037],"56e32e60":[()=>n.e(3711).then(n.t.bind(n,19623,19)),"~blog/default/blog-tags-authentication-b73-list.json",19623],"5706869d":[()=>n.e(8007).then(n.bind(n,24578)),"@site/versioned_docs/version-4/server/channel_token_auth.md",24578],"58246c43":[()=>n.e(7423).then(n.bind(n,10747)),"@site/versioned_docs/version-3/server/server_subs.md",10747],"58b29436":[()=>Promise.all([n.e(3312),n.e(9620)]).then(n.bind(n,74413)),"@site/docs/transports/client_api.md",74413],"5934e2d7":[()=>n.e(8982).then(n.bind(n,37570)),"@site/versioned_docs/version-4/faq/index.md",37570],"5de4a79c":[()=>Promise.all([n.e(3312),n.e(8375)]).then(n.bind(n,62158)),"@site/docs/server/authentication.md",62158],"5e95c892":[()=>n.e(9661).then(n.bind(n,93143)),"@theme/DocsRoot",93143],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,36809)),"@generated/docusaurus.config",36809],"5f78e650":[()=>n.e(6979).then(n.t.bind(n,74300,19)),"~blog/default/blog-tags-centrifugo-310.json",74300],"5fe24874":[()=>n.e(1420).then(n.bind(n,85834)),"@site/versioned_docs/version-4/server/load_balancing.md",85834],"5ffc8930":[()=>n.e(630).then(n.bind(n,55084)),"@site/versioned_docs/version-4/pro/performance.md",55084],"60271c2c":[()=>n.e(2906).then(n.bind(n,8599)),"@site/docs/server/console_commands.md",8599],"629b5641":[()=>n.e(509).then(n.bind(n,81400)),"@site/docs/server/proxy.md",81400],"631e3db1":[()=>n.e(4114).then(n.bind(n,698)),"@site/docs/server/server_api.md",698],"633b2ed2":[()=>n.e(4225).then(n.bind(n,79993)),"@site/docs/pro/distributed_rate_limit.md",79993],"64e125c9":[()=>n.e(4156).then(n.t.bind(n,34174,19)),"~blog/default/blog-tags-keycloak-6c4-list.json",34174],"6574fcee":[()=>n.e(6720).then(n.bind(n,9294)),"@site/docs/transports/client_sdk.md",9294],"66eb7538":[()=>n.e(7482).then(n.t.bind(n,6658,19)),"~blog/default/blog-tags-php-f9f.json",6658],"679046a6":[()=>n.e(5148).then(n.bind(n,6922)),"@site/versioned_docs/version-3/transports/websocket.md",6922],"6875c492":[()=>Promise.all([n.e(3312),n.e(8610)]).then(n.bind(n,45462)),"@theme/BlogTagsPostsPage",45462],"694566b3":[()=>n.e(3655).then(n.bind(n,73841)),"@site/docs/transports/sockjs.md",73841],"69d81c34":[()=>n.e(3352).then(n.bind(n,42271)),"@site/versioned_docs/version-4/server/server_subs.md",42271],"6aa961d8":[()=>n.e(3321).then(n.bind(n,96145)),"@site/docs/server/observability.md",96145],"6b2be476":[()=>n.e(2165).then(n.bind(n,62432)),"@site/versioned_docs/version-3/attributions.md",62432],"6e07cb60":[()=>n.e(2352).then(n.t.bind(n,11800,19)),"~blog/default/blog-tags-laravel-bcc-list.json",11800],"6e37598f":[()=>n.e(1196).then(n.bind(n,75548)),"@site/docs/pro/observability_enhancements.md",75548],"6e81f787":[()=>n.e(2605).then(n.t.bind(n,86212,19)),"~blog/default/blog-tags-proxy-b6a-list.json",86212],"6eaeadff":[()=>n.e(5421).then(n.t.bind(n,70393,19)),"~blog/default/blog-tags-interview-9a2.json",70393],"6ef9986d":[()=>n.e(2637).then(n.bind(n,45185)),"@site/blog/2020-02-10-million-connections-with-centrifugo.md?truncated=true",45185],"6fbe284c":[()=>n.e(7572).then(n.bind(n,92772)),"@site/docs/pro/user_status.md",92772],"70aa60b8":[()=>n.e(926).then(n.bind(n,45493)),"@site/versioned_docs/version-4/pro/throttling.md",45493],"71fc0044":[()=>n.e(2578).then(n.bind(n,47845)),"@site/blog/2023-08-19-asynchronous-message-streaming-to-centrifugo-with-benthos.md?truncated=true",47845],"73c943f6":[()=>n.e(8702).then(n.t.bind(n,30963,19)),"~blog/default/blog-tags-usecase-5d7-list.json",30963],"73e61bcc":[()=>n.e(9925).then(n.bind(n,76415)),"@site/docs/server/server_subs.md",76415],"7672fb2a":[()=>Promise.all([n.e(3312),n.e(8589)]).then(n.bind(n,99512)),"@site/versioned_docs/version-4/server/authentication.md",99512],"7747d83f":[()=>n.e(9878).then(n.bind(n,95114)),"@site/docs/pro/cel_expressions.md",95114],"776d934d":[()=>n.e(4655).then(n.bind(n,72919)),"@site/blog/2021-12-14-laravel-multi-room-chat-tutorial.md?truncated=true",72919],"77e23114":[()=>n.e(5074).then(n.t.bind(n,67217,19)),"~blog/default/blog-tags-tutorial-e7d.json",67217],"79276c30":[()=>n.e(8983).then(n.bind(n,71852)),"@site/versioned_docs/version-3/server/console_commands.md",71852],"79ee6175":[()=>n.e(1028).then(n.t.bind(n,20199,19)),"~blog/default/blog-tags-websocket-068-list.json",20199],"7a7ba156":[()=>n.e(2040).then(n.bind(n,28215)),"@site/versioned_docs/version-4/pro/install_and_run.md",28215],"7bd30152":[()=>n.e(2635).then(n.bind(n,56575)),"@site/versioned_docs/version-4/getting-started/migration-v4.md",56575],"7df4dfbf":[()=>n.e(3461).then(n.bind(n,44275)),"@site/docs/attributions.md",44275],"814f3328":[()=>n.e(2535).then(n.t.bind(n,45641,19)),"~blog/default/blog-post-list-prop-default.json",45641],"81e12894":[()=>n.e(6050).then(n.bind(n,31980)),"@site/versioned_docs/version-3/pro/process_stats.md",31980],"83d480e9":[()=>n.e(205).then(n.t.bind(n,43672,19)),"~blog/default/blog-tags-release-b5c.json",43672],"84a9b932":[()=>n.e(410).then(n.bind(n,38706)),"@site/versioned_docs/version-4/pro/user_block.md",38706],"84b8e2b1":[()=>n.e(477).then(n.bind(n,9086)),"@site/versioned_docs/version-3/pro/user_connections.md",9086],"85196f1f":[()=>n.e(5758).then(n.t.bind(n,478,19)),"~blog/default/blog-tags-centrifuge-ab6-list.json",478],"861598a7":[()=>n.e(6738).then(n.bind(n,76710)),"@site/docs/transports/uni_websocket.md",76710],"893c1918":[()=>n.e(811).then(n.bind(n,68625)),"@site/versioned_docs/version-3/server/server_api.md",68625],"89734ed6":[()=>n.e(5736).then(n.bind(n,27758)),"@site/versioned_docs/version-4/transports/websocket.md",27758],"8a978eb4":[()=>n.e(4901).then(n.t.bind(n,15795,19)),"~blog/default/blog-tags-websocket-068.json",15795],"8db697a0":[()=>n.e(9362).then(n.bind(n,57198)),"@site/versioned_docs/version-3/server/load_balancing.md",57198],"8e068dda":[()=>n.e(7566).then(n.bind(n,65413)),"@site/versioned_docs/version-3/pro/install_and_run.md",65413],"8e9fe0eb":[()=>n.e(8648).then(n.t.bind(n,58934,19)),"~docs/default/version-3-metadata-prop-ff3.json",58934],"91116fee":[()=>n.e(7670).then(n.bind(n,92263)),"@site/versioned_docs/version-3/pro/user_status.md",92263],"9179a2e2":[()=>n.e(5507).then(n.t.bind(n,56599,19)),"~blog/default/blog-tags-push-notifications-7de-list.json",56599],"91c5cac2":[()=>n.e(6877).then(n.bind(n,78249)),"@site/versioned_docs/version-3/pro/analytics.md",78249],"91fdfcd5":[()=>n.e(8599).then(n.t.bind(n,66384,19)),"~blog/default/blog-tags-sso-34a-list.json",66384],"92b58ac1":[()=>n.e(9247).then(n.bind(n,80187)),"@site/versioned_docs/version-3/server/infra_tuning.md",80187],"935f2afb":[()=>n.e(53).then(n.t.bind(n,1109,19)),"~docs/default/version-current-metadata-prop-751.json",1109],"936398dc":[()=>n.e(5107).then(n.bind(n,37690)),"@site/versioned_docs/version-3/server/configuration.md",37690],"93f9db65":[()=>n.e(6867).then(n.bind(n,14895)),"@site/versioned_docs/version-4/server/configuration.md",14895],"945c690d":[()=>n.e(2089).then(n.t.bind(n,42266,19)),"~blog/default/blog-tags-sso-34a.json",42266],"9475880e":[()=>n.e(1151).then(n.t.bind(n,61426,19)),"~blog/default/blog-tags-authentication-b73.json",61426],"94d5cf4c":[()=>n.e(5028).then(n.bind(n,59042)),"@site/docs/transports/websocket.md",59042],"984c0c66":[()=>n.e(3556).then(n.bind(n,48179)),"@site/blog/2023-08-19-asynchronous-message-streaming-to-centrifugo-with-benthos.md",48179],"9b70d0cc":[()=>n.e(9027).then(n.bind(n,93113)),"@site/blog/2021-10-18-integrating-with-nodejs.md?truncated=true",93113],"9b9e219e":[()=>n.e(8665).then(n.bind(n,7375)),"@site/versioned_docs/version-4/server/engines.md",7375],"9c021584":[()=>n.e(7438).then(n.t.bind(n,98055,19)),"~blog/default/blog-tags-release-b5c-list.json",98055],"9c1ee1d6":[()=>n.e(5861).then(n.bind(n,37512)),"@site/blog/2021-08-31-hello-centrifugo-v3.md",37512],"9c3b1acf":[()=>n.e(7270).then(n.bind(n,49583)),"@site/docs/tutorial/reverse_proxy.md",49583],"9c87bba9":[()=>n.e(9334).then(n.bind(n,15280)),"@site/versioned_docs/version-4/server/infra_tuning.md",15280],"9db3c45b":[()=>n.e(1977).then(n.bind(n,82946)),"@site/docs/pro/user_block.md",82946],"9dd8a0d2":[()=>Promise.all([n.e(3312),n.e(7054)]).then(n.bind(n,10140)),"@site/src/pages/index.jsx",10140],"9e4087bc":[()=>n.e(3608).then(n.bind(n,98265)),"@theme/BlogArchivePage",98265],"9ff4038f":[()=>n.e(2353).then(n.bind(n,4590)),"@site/docs/getting-started/introduction.md",4590],a05caef7:[()=>n.e(1752).then(n.t.bind(n,52955,19)),"~docs/default/version-4-metadata-prop-5ed.json",52955],a1538072:[()=>n.e(9109).then(n.bind(n,83622)),"@site/versioned_docs/version-3/transports/protocol.md",83622],a2d1b113:[()=>n.e(562).then(n.bind(n,50246)),"@site/versioned_docs/version-4/server/monitoring.md",50246],a41a0a70:[()=>n.e(6274).then(n.bind(n,87135)),"@site/blog/2023-03-31-keycloak-sso-centrifugo.md?truncated=true",87135],a4ddbaa1:[()=>n.e(2901).then(n.t.bind(n,50874,19)),"~blog/default/blog-tags-usecase-5d7.json",50874],a564e6ff:[()=>n.e(5755).then(n.bind(n,34627)),"@site/src/pages/license.md",34627],a6aa9e1f:[()=>Promise.all([n.e(3312),n.e(3089)]).then(n.bind(n,51895)),"@theme/BlogListPage",51895],a7023ddc:[()=>n.e(1713).then(n.t.bind(n,53457,19)),"~blog/default/blog-tags-tags-4c2.json",53457],a728857c:[()=>n.e(1530).then(n.bind(n,86202)),"@site/docs/transports/webtransport.md",86202],a74df3cd:[()=>n.e(6567).then(n.bind(n,96265)),"@site/versioned_docs/version-4/server/channels.md",96265],a7bd4aaa:[()=>n.e(8518).then(n.bind(n,72596)),"@theme/DocVersionRoot",72596],a82fa8b7:[()=>n.e(4435).then(n.bind(n,79857)),"@site/docs/transports/overview.md",79857],a8fc9e46:[()=>n.e(7445).then(n.bind(n,31082)),"@site/docs/pro/channel_events.md",31082],a94703ab:[()=>Promise.all([n.e(3312),n.e(4368)]).then(n.bind(n,43193)),"@theme/DocRoot",43193],aa73fb9a:[()=>n.e(5853).then(n.t.bind(n,24469,19)),"/Users/fz/projects/centrifugal/centrifugal.dev/.docusaurus/docusaurus-plugin-content-blog/default/plugin-route-context-module-100.json",24469],aacb0ae1:[()=>n.e(6889).then(n.bind(n,15953)),"@site/versioned_docs/version-3/pro/throttling.md",15953],ab6f12ff:[()=>n.e(7040).then(n.bind(n,49461)),"@site/blog/2023-03-31-keycloak-sso-centrifugo.md",49461],ab8e6500:[()=>n.e(624).then(n.t.bind(n,39770,19)),"~blog/default/blog-tags-benthos-fe1-list.json",39770],ad7c169e:[()=>n.e(2865).then(n.bind(n,24963)),"@site/blog/2023-10-29-discovering-centrifugo-pro-push-notifications.md?truncated=true",24963],b05011d9:[()=>n.e(7092).then(n.bind(n,61818)),"@site/blog/2023-10-29-discovering-centrifugo-pro-push-notifications.md",61818],b0ea8d09:[()=>n.e(8693).then(n.bind(n,46467)),"@site/docs/transports/http_stream.md",46467],b1f4df52:[()=>n.e(9347).then(n.bind(n,19713)),"@site/versioned_docs/version-4/transports/webtransport.md",19713],b2b675dd:[()=>n.e(533).then(n.t.bind(n,28017,19)),"~blog/default/blog-c06.json",28017],b2eaf182:[()=>n.e(3073).then(n.bind(n,35113)),"@site/versioned_docs/version-4/server/presence.md",35113],b2f554cd:[()=>n.e(1477).then(n.t.bind(n,30010,19)),"~blog/default/blog-archive-80c.json",30010],b301b932:[()=>n.e(1439).then(n.bind(n,27448)),"@site/versioned_docs/version-4/server/history_and_recovery.md",27448],b3216779:[()=>n.e(4566).then(n.t.bind(n,38786,19)),"~blog/default/blog-tags-centrifuge-ab6.json",38786],b325d9c4:[()=>n.e(408).then(n.bind(n,32471)),"@site/versioned_docs/version-4/pro/client_msg_batching.md",32471],b479c509:[()=>n.e(8499).then(n.bind(n,17548)),"@site/versioned_docs/version-4/transports/uni_websocket.md",17548],b4a3c16e:[()=>n.e(5734).then(n.bind(n,83074)),"@site/docs/server/consumers.md",83074],b4b43a34:[()=>n.e(4890).then(n.t.bind(n,45922,19)),"~blog/default/blog-tags-quic-5d2-list.json",45922],b4f0bebf:[()=>n.e(6953).then(n.bind(n,51584)),"@site/blog/2022-07-19-centrifugo-v4-released.md",51584],b5547432:[()=>n.e(7330).then(n.bind(n,92289)),"@site/docs/pro/process_stats.md",92289],b62a3811:[()=>n.e(9602).then(n.bind(n,92267)),"@site/versioned_docs/version-4/flow_diagrams.md",92267],b6f2a3eb:[()=>n.e(9474).then(n.bind(n,31662)),"@site/docs/getting-started/design.md",31662],b89c2c0a:[()=>n.e(9054).then(n.bind(n,52665)),"@site/versioned_docs/version-3/transports/client_sdk.md",52665],b910a859:[()=>n.e(1792).then(n.bind(n,362)),"@site/docs/tutorial/improvements.md",362],b9cceeee:[()=>n.e(7725).then(n.bind(n,40692)),"@site/docs/pro/connections.md",40692],ba0d3b30:[()=>n.e(2309).then(n.bind(n,40966)),"@site/versioned_docs/version-4/transports/overview.md",40966],bbb9e52d:[()=>n.e(9240).then(n.bind(n,7042)),"@site/docs/pro/install_and_run.md",7042],bbd14fff:[()=>n.e(3001).then(n.bind(n,2448)),"@site/versioned_docs/version-4/getting-started/design.md",2448],bd17143e:[()=>n.e(674).then(n.bind(n,37582)),"@site/blog/2023-08-29-using-centrifugo-in-rabbitx.md",37582],be4c395a:[()=>n.e(2128).then(n.bind(n,93381)),"@site/versioned_docs/version-4/server/admin_web.md",93381],bfbfeea3:[()=>n.e(2438).then(n.bind(n,97322)),"@site/src/pages/components/logos/Grafana.js",97322],c0434fb9:[()=>n.e(3676).then(n.bind(n,12262)),"@site/versioned_docs/version-3/server/engines.md",12262],c1817076:[()=>n.e(5847).then(n.bind(n,93173)),"@site/versioned_docs/version-4/transports/client_protocol.md",93173],c2f60b05:[()=>n.e(7049).then(n.bind(n,69999)),"@site/versioned_docs/version-3/server/channels.md",69999],c318ab3c:[()=>n.e(5214).then(n.bind(n,80889)),"@site/versioned_docs/version-4/getting-started/integration.md",80889],c3677326:[()=>n.e(2569).then(n.bind(n,86746)),"@site/blog/2020-10-16-experimenting-with-quic-transport.md",86746],c5dc0dc4:[()=>n.e(7824).then(n.t.bind(n,6774,19)),"~blog/default/blog-tags-pro-ddd-list.json",6774],c8380abd:[()=>n.e(8655).then(n.bind(n,11698)),"@site/versioned_docs/version-3/getting-started/design.md",11698],c98fa109:[()=>n.e(4586).then(n.bind(n,52270)),"@site/docs/transports/client_protocol.md",52270],c9a3329e:[()=>n.e(430).then(n.bind(n,62093)),"@site/docs/server/proxy_streams.md",62093],cac93e67:[()=>n.e(3165).then(n.bind(n,16116)),"@site/versioned_docs/version-4/server/tls.md",16116],cadfeb4f:[()=>n.e(8650).then(n.bind(n,14293)),"@site/docs/server/infra_tuning.md",14293],cc50533f:[()=>n.e(7578).then(n.bind(n,9804)),"@site/docs/tutorial/outro.md",9804],ccc49370:[()=>Promise.all([n.e(3312),n.e(1343),n.e(7264),n.e(6103)]).then(n.bind(n,334)),"@theme/BlogPostPage",334],cce51cf2:[()=>n.e(6447).then(n.t.bind(n,42932,19)),"~blog/default/blog-tags-go-099-list.json",42932],cf0e38ba:[()=>n.e(2069).then(n.bind(n,37725)),"@site/docs/tutorial/tips_and_tricks.md",37725],d016d150:[()=>n.e(5180).then(n.bind(n,73413)),"@site/versioned_docs/version-4/getting-started/client_api.md",73413],d1c7a4f7:[()=>n.e(2469).then(n.bind(n,7443)),"@site/docs/pro/push_notifications.md",7443],d1cb7448:[()=>n.e(532).then(n.bind(n,4528)),"@site/versioned_docs/version-4/getting-started/highlights.md",4528],d2c1944d:[()=>n.e(1695).then(n.bind(n,84045)),"@site/docs/pro/channel_patterns.md",84045],d2fe6fea:[()=>Promise.all([n.e(3312),n.e(5901)]).then(n.bind(n,75467)),"@site/docs/server/channel_token_auth.md",75467],d4c029c2:[()=>n.e(8973).then(n.bind(n,18732)),"@site/docs/tutorial/frontend.md",18732],d4ca9753:[()=>n.e(902).then(n.bind(n,95315)),"@site/versioned_docs/version-3/getting-started/client_api.md",95315],d4dfc5db:[()=>n.e(1888).then(n.bind(n,66674)),"@site/docs/getting-started/integration.md",66674],d6627831:[()=>n.e(1983).then(n.bind(n,98471)),"@site/blog/2022-07-29-101-way-to-subscribe.md?truncated=true",98471],d835c886:[()=>n.e(5878).then(n.bind(n,93964)),"@site/docs/getting-started/comparisons.md",93964],d9829201:[()=>n.e(4016).then(n.bind(n,48375)),"@site/versioned_docs/version-4/getting-started/quickstart.md",48375],db2f115c:[()=>n.e(6685).then(n.bind(n,22519)),"@site/versioned_docs/version-3/server/admin_web.md",22519],dc4f2258:[()=>n.e(70).then(n.bind(n,14213)),"@site/versioned_docs/version-3/flow_diagrams.md",14213],dd818855:[()=>n.e(2157).then(n.t.bind(n,20606,19)),"~blog/default/blog-tags-php-f9f-list.json",20606],e19d40c8:[()=>n.e(7140).then(n.bind(n,41789)),"@site/docs/transports/uni_http_stream.md",41789],e257283f:[()=>n.e(8791).then(n.bind(n,19589)),"@site/docs/pro/capabilities.md",19589],e66faea1:[()=>n.e(7262).then(n.bind(n,81882)),"@site/blog/2022-07-19-centrifugo-v4-released.md?truncated=true",81882],e6afaed9:[()=>n.e(4491).then(n.bind(n,27747)),"@site/versioned_docs/version-3/faq/index.md",27747],e6b6a8f8:[()=>n.e(4282).then(n.bind(n,42429)),"@site/docs/server/tls.md",42429],e7893f84:[()=>n.e(9235).then(n.bind(n,92146)),"@site/versioned_docs/version-3/pro/db_namespaces.md",92146],e8314be4:[()=>n.e(4020).then(n.bind(n,90322)),"@site/versioned_docs/version-4/pro/connections.md",90322],e9cbd346:[()=>n.e(9630).then(n.bind(n,73061)),"@site/versioned_docs/version-4/pro/process_stats.md",73061],ea108d2f:[()=>n.e(4964).then(n.bind(n,25273)),"@site/docs/pro/rate_limiting.md",25273],ecd5d374:[()=>n.e(6295).then(n.t.bind(n,98114,19)),"~blog/default/blog-tags-keycloak-6c4.json",98114],ed785809:[()=>n.e(2479).then(n.bind(n,36230)),"@site/blog/2020-11-12-scaling-websocket.md?truncated=true",36230],ee10dcb9:[()=>n.e(4864).then(n.bind(n,11433)),"@site/docs/transports/sse.md",11433],ee78c395:[()=>n.e(5484).then(n.bind(n,88341)),"@site/docs/pro/overview.md",88341],ee88d6e4:[()=>n.e(2442).then(n.bind(n,19471)),"@site/docs/getting-started/migration-v5.md",19471],f178572b:[()=>n.e(5863).then(n.bind(n,8260)),"@site/blog/2020-11-12-scaling-websocket.md",8260],f1b7a7af:[()=>n.e(1994).then(n.bind(n,92818)),"@site/blog/2021-01-15-centrifuge-intro.md",92818],f26176d2:[()=>n.e(3468).then(n.bind(n,3335)),"@site/blog/2021-11-04-integrating-with-django-building-chat-application.md",3335],f2e9cf2f:[()=>n.e(7318).then(n.bind(n,15547)),"@site/docs/getting-started/ecosystem.md",15547],f2f7592a:[()=>n.e(8523).then(n.bind(n,48031)),"@site/versioned_docs/version-3/getting-started/highlights.md",48031],f346273e:[()=>n.e(6386).then(n.t.bind(n,15745,19)),"/Users/fz/projects/centrifugal/centrifugal.dev/.docusaurus/docusaurus-plugin-content-pages/default/plugin-route-context-module-100.json",15745],f4f2dadf:[()=>n.e(7660).then(n.t.bind(n,93920,19)),"~blog/default/blog-tags-django-c32-list.json",93920],f762c5da:[()=>n.e(6562).then(n.bind(n,33052)),"@site/blog/2023-06-29-centrifugo-v5-released.md",33052],f90eb0d6:[()=>n.e(1178).then(n.t.bind(n,35467,19)),"~blog/default/blog-tags-webtransport-009.json",35467],fbd7a87c:[()=>n.e(3039).then(n.bind(n,93174)),"@site/docs/getting-started/quickstart.md",93174],fc3deafd:[()=>n.e(7659).then(n.t.bind(n,54568,19)),"~blog/default/blog-tags-go-099.json",54568],fcb790ab:[()=>n.e(6515).then(n.bind(n,5232)),"@site/versioned_docs/version-3/transports/overview.md",5232],fd1fdc14:[()=>n.e(9565).then(n.t.bind(n,15164,19)),"~blog/default/blog-tags-redis-216-list.json",15164],fd3209d2:[()=>n.e(5312).then(n.bind(n,96149)),"@site/blog/2021-08-31-hello-centrifugo-v3.md?truncated=true",96149],fd93cfee:[()=>Promise.all([n.e(3312),n.e(9217)]).then(n.bind(n,85279)),"@site/src/pages/components/Highlight.js",85279],fe52e117:[()=>n.e(6705).then(n.bind(n,6759)),"@site/versioned_docs/version-3/ecosystem/integrations.md",6759],fe6343fd:[()=>n.e(1002).then(n.bind(n,94187)),"@site/docs/faq/index.md",94187],fe91fc6f:[()=>n.e(7765).then(n.bind(n,29761)),"@site/blog/2021-11-04-integrating-with-django-building-chat-application.md?truncated=true",29761],ff64321a:[()=>n.e(2735).then(n.bind(n,17508)),"@site/blog/2022-12-20-improving-redis-engine-performance.md?truncated=true",17508],ffdd667d:[()=>n.e(5146).then(n.bind(n,85949)),"@site/blog/2021-10-18-integrating-with-nodejs.md",85949]};var i=n(85893);function c(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,i.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,i.jsx)("p",{children:String(t)}),(0,i.jsx)("div",{children:(0,i.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,i.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,i.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,i.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,i.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,i.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,i.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,i.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,i.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,i.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,i.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,i.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,i.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,i.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var l=n(66916),u=n(66041);function d(e,t){if("*"===e)return o()({loading:c,loader:()=>n.e(5072).then(n.bind(n,25072)),modules:["@theme/NotFound"],webpack:()=>[25072],render(e,t){const n=e.default;return(0,i.jsx)(u.z,{value:{plugin:{name:"native",id:"default"}},children:(0,i.jsx)(n,{...t})})}});const r=a[`${e}-${t}`],d={},p=[],f=[],g=(0,l.Z)(r);return Object.entries(g).forEach((e=>{let[t,n]=e;const r=s[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),o().Map({loading:c,loader:d,modules:p,webpack:()=>f,render(t,n){const o=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let s=o;const i=n.split(".");i.slice(0,-1).forEach((e=>{s=s[e]})),s[i[i.length-1]]=a}));const a=o.__comp;delete o.__comp;const s=o.__context;return delete o.__context,(0,i.jsx)(u.z,{value:s,children:(0,i.jsx)(a,{...o,...n})})}})}const p=[{path:"/blog",component:d("/blog","ec4"),exact:!0},{path:"/blog/2020/02/10/million-connections-with-centrifugo",component:d("/blog/2020/02/10/million-connections-with-centrifugo","f79"),exact:!0},{path:"/blog/2020/10/16/experimenting-with-quic-transport",component:d("/blog/2020/10/16/experimenting-with-quic-transport","a22"),exact:!0},{path:"/blog/2020/11/12/scaling-websocket",component:d("/blog/2020/11/12/scaling-websocket","72d"),exact:!0},{path:"/blog/2021/01/15/centrifuge-intro",component:d("/blog/2021/01/15/centrifuge-intro","842"),exact:!0},{path:"/blog/2021/08/31/hello-centrifugo-v3",component:d("/blog/2021/08/31/hello-centrifugo-v3","bc5"),exact:!0},{path:"/blog/2021/10/18/integrating-with-nodejs",component:d("/blog/2021/10/18/integrating-with-nodejs","549"),exact:!0},{path:"/blog/2021/11/04/integrating-with-django-building-chat-application",component:d("/blog/2021/11/04/integrating-with-django-building-chat-application","173"),exact:!0},{path:"/blog/2021/12/14/laravel-multi-room-chat-tutorial",component:d("/blog/2021/12/14/laravel-multi-room-chat-tutorial","0dc"),exact:!0},{path:"/blog/2022/07/19/centrifugo-v4-released",component:d("/blog/2022/07/19/centrifugo-v4-released","315"),exact:!0},{path:"/blog/2022/07/29/101-way-to-subscribe",component:d("/blog/2022/07/29/101-way-to-subscribe","771"),exact:!0},{path:"/blog/2022/12/20/improving-redis-engine-performance",component:d("/blog/2022/12/20/improving-redis-engine-performance","0a0"),exact:!0},{path:"/blog/2023/03/31/keycloak-sso-centrifugo",component:d("/blog/2023/03/31/keycloak-sso-centrifugo","b07"),exact:!0},{path:"/blog/2023/06/29/centrifugo-v5-released",component:d("/blog/2023/06/29/centrifugo-v5-released","182"),exact:!0},{path:"/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos",component:d("/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos","080"),exact:!0},{path:"/blog/2023/08/29/using-centrifugo-in-rabbitx",component:d("/blog/2023/08/29/using-centrifugo-in-rabbitx","699"),exact:!0},{path:"/blog/2023/10/29/discovering-centrifugo-pro-push-notifications",component:d("/blog/2023/10/29/discovering-centrifugo-pro-push-notifications","eb3"),exact:!0},{path:"/blog/archive",component:d("/blog/archive","067"),exact:!0},{path:"/blog/tags",component:d("/blog/tags","916"),exact:!0},{path:"/blog/tags/authentication",component:d("/blog/tags/authentication","75d"),exact:!0},{path:"/blog/tags/benthos",component:d("/blog/tags/benthos","b03"),exact:!0},{path:"/blog/tags/centrifuge",component:d("/blog/tags/centrifuge","638"),exact:!0},{path:"/blog/tags/centrifugo",component:d("/blog/tags/centrifugo","327"),exact:!0},{path:"/blog/tags/django",component:d("/blog/tags/django","c69"),exact:!0},{path:"/blog/tags/go",component:d("/blog/tags/go","967"),exact:!0},{path:"/blog/tags/interview",component:d("/blog/tags/interview","f50"),exact:!0},{path:"/blog/tags/keycloak",component:d("/blog/tags/keycloak","6f7"),exact:!0},{path:"/blog/tags/laravel",component:d("/blog/tags/laravel","6d7"),exact:!0},{path:"/blog/tags/php",component:d("/blog/tags/php","853"),exact:!0},{path:"/blog/tags/pro",component:d("/blog/tags/pro","59d"),exact:!0},{path:"/blog/tags/proxy",component:d("/blog/tags/proxy","218"),exact:!0},{path:"/blog/tags/push-notifications",component:d("/blog/tags/push-notifications","e69"),exact:!0},{path:"/blog/tags/quic",component:d("/blog/tags/quic","5f7"),exact:!0},{path:"/blog/tags/redis",component:d("/blog/tags/redis","3d3"),exact:!0},{path:"/blog/tags/release",component:d("/blog/tags/release","db7"),exact:!0},{path:"/blog/tags/sso",component:d("/blog/tags/sso","ab7"),exact:!0},{path:"/blog/tags/tutorial",component:d("/blog/tags/tutorial","571"),exact:!0},{path:"/blog/tags/usecase",component:d("/blog/tags/usecase","54d"),exact:!0},{path:"/blog/tags/websocket",component:d("/blog/tags/websocket","b64"),exact:!0},{path:"/blog/tags/webtransport",component:d("/blog/tags/webtransport","f50"),exact:!0},{path:"/components/Highlight",component:d("/components/Highlight","c2c"),exact:!0},{path:"/components/logo",component:d("/components/logo","e9f"),exact:!0},{path:"/components/logos/Badoo",component:d("/components/logos/Badoo","9f0"),exact:!0},{path:"/components/logos/Grafana",component:d("/components/logos/Grafana","56e"),exact:!0},{path:"/components/logos/ManyChat",component:d("/components/logos/ManyChat","34c"),exact:!0},{path:"/components/logos/OpenWeb",component:d("/components/logos/OpenWeb","8e3"),exact:!0},{path:"/license",component:d("/license","85e"),exact:!0},{path:"/license_exchange_lemon",component:d("/license_exchange_lemon","c94"),exact:!0},{path:"/search",component:d("/search","af8"),exact:!0},{path:"/docs",component:d("/docs","5af"),routes:[{path:"/docs/3",component:d("/docs/3","626"),routes:[{path:"/docs/3",component:d("/docs/3","be8"),routes:[{path:"/docs/3/attributions",component:d("/docs/3/attributions","5e5"),exact:!0},{path:"/docs/3/ecosystem/centrifuge",component:d("/docs/3/ecosystem/centrifuge","c8e"),exact:!0,sidebar:"Ecosystem"},{path:"/docs/3/ecosystem/integrations",component:d("/docs/3/ecosystem/integrations","435"),exact:!0,sidebar:"Ecosystem"},{path:"/docs/3/faq",component:d("/docs/3/faq","48c"),exact:!0},{path:"/docs/3/flow_diagrams",component:d("/docs/3/flow_diagrams","1fb"),exact:!0},{path:"/docs/3/getting-started/client_api",component:d("/docs/3/getting-started/client_api","78b"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/design",component:d("/docs/3/getting-started/design","cec"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/highlights",component:d("/docs/3/getting-started/highlights","cfe"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/installation",component:d("/docs/3/getting-started/installation","c82"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/integration",component:d("/docs/3/getting-started/integration","e48"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/introduction",component:d("/docs/3/getting-started/introduction","e4d"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/migration_v3",component:d("/docs/3/getting-started/migration_v3","6db"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/getting-started/quickstart",component:d("/docs/3/getting-started/quickstart","f1c"),exact:!0,sidebar:"Introduction"},{path:"/docs/3/pro/analytics",component:d("/docs/3/pro/analytics","1be"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/db_namespaces",component:d("/docs/3/pro/db_namespaces","db6"),exact:!0},{path:"/docs/3/pro/install_and_run",component:d("/docs/3/pro/install_and_run","70a"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/overview",component:d("/docs/3/pro/overview","553"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/performance",component:d("/docs/3/pro/performance","500"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/process_stats",component:d("/docs/3/pro/process_stats","63b"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/singleflight",component:d("/docs/3/pro/singleflight","28b"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/throttling",component:d("/docs/3/pro/throttling","6c0"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/token_revocation",component:d("/docs/3/pro/token_revocation","fd1"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/tracing",component:d("/docs/3/pro/tracing","39c"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/user_block",component:d("/docs/3/pro/user_block","23e"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/user_connections",component:d("/docs/3/pro/user_connections","ed7"),exact:!0,sidebar:"Pro"},{path:"/docs/3/pro/user_status",component:d("/docs/3/pro/user_status","3be"),exact:!0,sidebar:"Pro"},{path:"/docs/3/server/admin_web",component:d("/docs/3/server/admin_web","0b3"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/authentication",component:d("/docs/3/server/authentication","9d5"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/channels",component:d("/docs/3/server/channels","4e7"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/codes",component:d("/docs/3/server/codes","507"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/configuration",component:d("/docs/3/server/configuration","188"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/console_commands",component:d("/docs/3/server/console_commands","71f"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/engines",component:d("/docs/3/server/engines","6fd"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/history_and_recovery",component:d("/docs/3/server/history_and_recovery","e35"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/infra_tuning",component:d("/docs/3/server/infra_tuning","0dc"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/load_balancing",component:d("/docs/3/server/load_balancing","2b3"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/monitoring",component:d("/docs/3/server/monitoring","b84"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/private_channels",component:d("/docs/3/server/private_channels","122"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/proxy",component:d("/docs/3/server/proxy","c52"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/server_api",component:d("/docs/3/server/server_api","992"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/server_subs",component:d("/docs/3/server/server_subs","9b2"),exact:!0,sidebar:"Guides"},{path:"/docs/3/server/tls",component:d("/docs/3/server/tls","256"),exact:!0,sidebar:"Guides"},{path:"/docs/3/transports/client_protocol",component:d("/docs/3/transports/client_protocol","fe1"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/client_sdk",component:d("/docs/3/transports/client_sdk","270"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/overview",component:d("/docs/3/transports/overview","2cc"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/sockjs",component:d("/docs/3/transports/sockjs","241"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/uni_grpc",component:d("/docs/3/transports/uni_grpc","53c"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/uni_http_stream",component:d("/docs/3/transports/uni_http_stream","93f"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/uni_sse",component:d("/docs/3/transports/uni_sse","6e7"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/uni_websocket",component:d("/docs/3/transports/uni_websocket","a2f"),exact:!0,sidebar:"Transports"},{path:"/docs/3/transports/websocket",component:d("/docs/3/transports/websocket","2cf"),exact:!0,sidebar:"Transports"}]}]},{path:"/docs/4",component:d("/docs/4","c58"),routes:[{path:"/docs/4",component:d("/docs/4","3d2"),routes:[{path:"/docs/4/attributions",component:d("/docs/4/attributions","78d"),exact:!0},{path:"/docs/4/faq",component:d("/docs/4/faq","e27"),exact:!0},{path:"/docs/4/flow_diagrams",component:d("/docs/4/flow_diagrams","e83"),exact:!0},{path:"/docs/4/getting-started/client_api",component:d("/docs/4/getting-started/client_api","1fe"),exact:!0},{path:"/docs/4/getting-started/community",component:d("/docs/4/getting-started/community","497"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/design",component:d("/docs/4/getting-started/design","358"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/ecosystem",component:d("/docs/4/getting-started/ecosystem","2a8"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/highlights",component:d("/docs/4/getting-started/highlights","510"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/installation",component:d("/docs/4/getting-started/installation","258"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/integration",component:d("/docs/4/getting-started/integration","77f"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/introduction",component:d("/docs/4/getting-started/introduction","68c"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/migration_v4",component:d("/docs/4/getting-started/migration_v4","13a"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/getting-started/quickstart",component:d("/docs/4/getting-started/quickstart","d9a"),exact:!0,sidebar:"Introduction"},{path:"/docs/4/pro/analytics",component:d("/docs/4/pro/analytics","805"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/capabilities",component:d("/docs/4/pro/capabilities","380"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/cel_expressions",component:d("/docs/4/pro/cel_expressions","d54"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/channel_patterns",component:d("/docs/4/pro/channel_patterns","270"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/client_message_batching",component:d("/docs/4/pro/client_message_batching","693"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/connections",component:d("/docs/4/pro/connections","fd4"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/install_and_run",component:d("/docs/4/pro/install_and_run","93c"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/overview",component:d("/docs/4/pro/overview","062"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/performance",component:d("/docs/4/pro/performance","e45"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/process_stats",component:d("/docs/4/pro/process_stats","728"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/push_notifications",component:d("/docs/4/pro/push_notifications","263"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/singleflight",component:d("/docs/4/pro/singleflight","8e2"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/throttling",component:d("/docs/4/pro/throttling","7b4"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/token_revocation",component:d("/docs/4/pro/token_revocation","0e5"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/tracing",component:d("/docs/4/pro/tracing","509"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/user_block",component:d("/docs/4/pro/user_block","4d4"),exact:!0,sidebar:"Pro"},{path:"/docs/4/pro/user_status",component:d("/docs/4/pro/user_status","027"),exact:!0,sidebar:"Pro"},{path:"/docs/4/server/admin_web",component:d("/docs/4/server/admin_web","7bb"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/authentication",component:d("/docs/4/server/authentication","f51"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/channel_permissions",component:d("/docs/4/server/channel_permissions","0d0"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/channel_token_auth",component:d("/docs/4/server/channel_token_auth","58a"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/channels",component:d("/docs/4/server/channels","d83"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/codes",component:d("/docs/4/server/codes","33f"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/configuration",component:d("/docs/4/server/configuration","726"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/console_commands",component:d("/docs/4/server/console_commands","a1b"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/engines",component:d("/docs/4/server/engines","66f"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/history_and_recovery",component:d("/docs/4/server/history_and_recovery","880"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/infra_tuning",component:d("/docs/4/server/infra_tuning","2cf"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/load_balancing",component:d("/docs/4/server/load_balancing","9eb"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/monitoring",component:d("/docs/4/server/monitoring","373"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/presence",component:d("/docs/4/server/presence","673"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/proxy",component:d("/docs/4/server/proxy","57e"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/server_api",component:d("/docs/4/server/server_api","0cd"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/server_subs",component:d("/docs/4/server/server_subs","18c"),exact:!0,sidebar:"Guides"},{path:"/docs/4/server/tls",component:d("/docs/4/server/tls","a0d"),exact:!0,sidebar:"Guides"},{path:"/docs/4/transports/client_api",component:d("/docs/4/transports/client_api","7e0"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/client_protocol",component:d("/docs/4/transports/client_protocol","ea0"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/client_sdk",component:d("/docs/4/transports/client_sdk","ac3"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/http_stream",component:d("/docs/4/transports/http_stream","397"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/overview",component:d("/docs/4/transports/overview","2e3"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/sockjs",component:d("/docs/4/transports/sockjs","b46"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/sse",component:d("/docs/4/transports/sse","e09"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/uni_grpc",component:d("/docs/4/transports/uni_grpc","b4f"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/uni_http_stream",component:d("/docs/4/transports/uni_http_stream","c15"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/uni_sse",component:d("/docs/4/transports/uni_sse","f99"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/uni_websocket",component:d("/docs/4/transports/uni_websocket","4e3"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/websocket",component:d("/docs/4/transports/websocket","89d"),exact:!0,sidebar:"Transports"},{path:"/docs/4/transports/webtransport",component:d("/docs/4/transports/webtransport","ad6"),exact:!0,sidebar:"Transports"}]}]},{path:"/docs",component:d("/docs","ebe"),routes:[{path:"/docs",component:d("/docs","458"),routes:[{path:"/docs/attributions",component:d("/docs/attributions","680"),exact:!0},{path:"/docs/faq",component:d("/docs/faq","4ac"),exact:!0},{path:"/docs/flow_diagrams",component:d("/docs/flow_diagrams","c9a"),exact:!0},{path:"/docs/getting-started/community",component:d("/docs/getting-started/community","ea2"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/comparisons",component:d("/docs/getting-started/comparisons","48a"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/design",component:d("/docs/getting-started/design","98d"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/ecosystem",component:d("/docs/getting-started/ecosystem","23a"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/highlights",component:d("/docs/getting-started/highlights","6b2"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/installation",component:d("/docs/getting-started/installation","23b"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/integration",component:d("/docs/getting-started/integration","bf9"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/introduction",component:d("/docs/getting-started/introduction","46a"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/migration_v4",component:d("/docs/getting-started/migration_v4","d84"),exact:!0},{path:"/docs/getting-started/migration_v5",component:d("/docs/getting-started/migration_v5","62a"),exact:!0,sidebar:"Introduction"},{path:"/docs/getting-started/quickstart",component:d("/docs/getting-started/quickstart","953"),exact:!0,sidebar:"Introduction"},{path:"/docs/pro/admin_idp_auth",component:d("/docs/pro/admin_idp_auth","9ac"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/analytics",component:d("/docs/pro/analytics","2ef"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/capabilities",component:d("/docs/pro/capabilities","5ec"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/cel_expressions",component:d("/docs/pro/cel_expressions","4ac"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/channel_patterns",component:d("/docs/pro/channel_patterns","25f"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/channel_state_events",component:d("/docs/pro/channel_state_events","945"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/client_message_batching",component:d("/docs/pro/client_message_batching","ad7"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/connections",component:d("/docs/pro/connections","782"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/distributed_rate_limit",component:d("/docs/pro/distributed_rate_limit","561"),exact:!0},{path:"/docs/pro/install_and_run",component:d("/docs/pro/install_and_run","c29"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/observability_enhancements",component:d("/docs/pro/observability_enhancements","d3e"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/overview",component:d("/docs/pro/overview","bfb"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/performance",component:d("/docs/pro/performance","21b"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/process_stats",component:d("/docs/pro/process_stats","daf"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/push_notifications",component:d("/docs/pro/push_notifications","2df"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/rate_limiting",component:d("/docs/pro/rate_limiting","e7b"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/singleflight",component:d("/docs/pro/singleflight","bbb"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/token_revocation",component:d("/docs/pro/token_revocation","a4c"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/tracing",component:d("/docs/pro/tracing","733"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/user_block",component:d("/docs/pro/user_block","30b"),exact:!0,sidebar:"Pro"},{path:"/docs/pro/user_status",component:d("/docs/pro/user_status","d1c"),exact:!0,sidebar:"Pro"},{path:"/docs/server/admin_web",component:d("/docs/server/admin_web","de5"),exact:!0,sidebar:"Guides"},{path:"/docs/server/authentication",component:d("/docs/server/authentication","5e6"),exact:!0,sidebar:"Guides"},{path:"/docs/server/channel_permissions",component:d("/docs/server/channel_permissions","09d"),exact:!0,sidebar:"Guides"},{path:"/docs/server/channel_token_auth",component:d("/docs/server/channel_token_auth","235"),exact:!0,sidebar:"Guides"},{path:"/docs/server/channels",component:d("/docs/server/channels","b28"),exact:!0,sidebar:"Guides"},{path:"/docs/server/codes",component:d("/docs/server/codes","789"),exact:!0,sidebar:"Guides"},{path:"/docs/server/configuration",component:d("/docs/server/configuration","96e"),exact:!0,sidebar:"Guides"},{path:"/docs/server/console_commands",component:d("/docs/server/console_commands","28f"),exact:!0,sidebar:"Guides"},{path:"/docs/server/consumers",component:d("/docs/server/consumers","51a"),exact:!0,sidebar:"Guides"},{path:"/docs/server/engines",component:d("/docs/server/engines","9d3"),exact:!0,sidebar:"Guides"},{path:"/docs/server/history_and_recovery",component:d("/docs/server/history_and_recovery","f28"),exact:!0,sidebar:"Guides"},{path:"/docs/server/infra_tuning",component:d("/docs/server/infra_tuning","789"),exact:!0,sidebar:"Guides"},{path:"/docs/server/load_balancing",component:d("/docs/server/load_balancing","10b"),exact:!0,sidebar:"Guides"},{path:"/docs/server/monitoring",component:d("/docs/server/monitoring","dd5"),exact:!0},{path:"/docs/server/observability",component:d("/docs/server/observability","02f"),exact:!0,sidebar:"Guides"},{path:"/docs/server/presence",component:d("/docs/server/presence","fbc"),exact:!0,sidebar:"Guides"},{path:"/docs/server/proxy",component:d("/docs/server/proxy","a46"),exact:!0,sidebar:"Guides"},{path:"/docs/server/proxy_streams",component:d("/docs/server/proxy_streams","1d7"),exact:!0,sidebar:"Guides"},{path:"/docs/server/server_api",component:d("/docs/server/server_api","e8a"),exact:!0,sidebar:"Guides"},{path:"/docs/server/server_subs",component:d("/docs/server/server_subs","70f"),exact:!0,sidebar:"Guides"},{path:"/docs/server/tls",component:d("/docs/server/tls","03d"),exact:!0,sidebar:"Guides"},{path:"/docs/transports/client_api",component:d("/docs/transports/client_api","2db"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/client_protocol",component:d("/docs/transports/client_protocol","528"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/client_sdk",component:d("/docs/transports/client_sdk","152"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/http_stream",component:d("/docs/transports/http_stream","92d"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/overview",component:d("/docs/transports/overview","d7d"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/sockjs",component:d("/docs/transports/sockjs","f81"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/sse",component:d("/docs/transports/sse","044"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/uni_client_protocol",component:d("/docs/transports/uni_client_protocol","364"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/uni_grpc",component:d("/docs/transports/uni_grpc","fd0"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/uni_http_stream",component:d("/docs/transports/uni_http_stream","38b"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/uni_sse",component:d("/docs/transports/uni_sse","a2b"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/uni_websocket",component:d("/docs/transports/uni_websocket","c20"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/websocket",component:d("/docs/transports/websocket","44c"),exact:!0,sidebar:"Transports"},{path:"/docs/transports/webtransport",component:d("/docs/transports/webtransport","527"),exact:!0,sidebar:"Transports"},{path:"/docs/tutorial/backend",component:d("/docs/tutorial/backend","176"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/centrifugo",component:d("/docs/tutorial/centrifugo","d63"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/frontend",component:d("/docs/tutorial/frontend","290"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/improvements",component:d("/docs/tutorial/improvements","f01"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/intro",component:d("/docs/tutorial/intro","f35"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/layout",component:d("/docs/tutorial/layout","f05"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/outbox_cdc",component:d("/docs/tutorial/outbox_cdc","e42"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/outro",component:d("/docs/tutorial/outro","686"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/recovery",component:d("/docs/tutorial/recovery","967"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/reverse_proxy",component:d("/docs/tutorial/reverse_proxy","1c1"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/scale",component:d("/docs/tutorial/scale","bd5"),exact:!0,sidebar:"Tutorial"},{path:"/docs/tutorial/tips_and_tricks",component:d("/docs/tutorial/tips_and_tricks","ea3"),exact:!0,sidebar:"Tutorial"}]}]}]},{path:"/",component:d("/","dc5"),exact:!0},{path:"*",component:d("*")}]},74058:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,t:()=>s});var r=n(67294),o=n(85893);const a=r.createContext(!1);function s(e){let{children:t}=e;const[n,s]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{s(!0)}),[]),(0,o.jsx)(a.Provider,{value:n,children:t})}},45227:(e,t,n)=>{"use strict";var r=n(67294),o=n(20745),a=n(73727),s=n(70405),i=n(19901);const c=[n(25557),n(32497),n(25529),n(26126),n(52295)];var l=n(21204),u=n(16550),d=n(18790),p=n(85893);function f(e){let{children:t}=e;return(0,p.jsx)(p.Fragment,{children:t})}var g=n(32411),h=n(6832),m=n(51402),b=n(96793),v=n(44873),y=n(13156),_=n(22768),w=n(39105),x=n(79861),k=n(26145);function S(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,h.Z)(),r=(0,y.l)(),o=n[e].htmlLang,a=e=>e.replace("-","_");return(0,p.jsxs)(g.Z,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,p.jsx)("meta",{property:"og:locale",content:a(o)}),Object.values(n).filter((e=>o!==e.htmlLang)).map((e=>(0,p.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function E(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,h.Z)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,h.Z)(),{pathname:r}=(0,u.TH)();return e+(0,x.applyTrailingSlash)((0,m.Z)(r),{trailingSlash:n,baseUrl:t})}(),o=t?`${n}${t}`:r;return(0,p.jsxs)(g.Z,{children:[(0,p.jsx)("meta",{property:"og:url",content:o}),(0,p.jsx)("link",{rel:"canonical",href:o})]})}function C(){const{i18n:{currentLocale:e}}=(0,h.Z)(),{metadata:t,image:n}=(0,b.L)();return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(g.Z,{children:[(0,p.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,p.jsx)("body",{className:_.h})]}),n&&(0,p.jsx)(v.d,{image:n}),(0,p.jsx)(E,{}),(0,p.jsx)(S,{}),(0,p.jsx)(k.Z,{tag:w.HX,locale:e}),(0,p.jsx)(g.Z,{children:t.map(((e,t)=>(0,p.jsx)("meta",{...e},t)))})]})}const T=new Map;function P(e){if(T.has(e.pathname))return{...e,pathname:T.get(e.pathname)};if((0,d.f)(l.Z,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return T.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return T.set(e.pathname,t),{...e,pathname:t}}var j=n(74058),A=n(56725),L=n(20613);function N(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r {const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>o.forEach((e=>e?.()))}const I=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,L.Z)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,o=t.hash===n.hash,a=t.search===n.search;if(r&&o&&!a)return;const{hash:s}=t;if(s){const e=decodeURIComponent(s.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),N("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function R(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,d.f)(l.Z,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class O extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=i.Z.canUseDOM?N("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=N("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),R(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,p.jsx)(I,{previousLocation:this.previousLocation,location:t,children:(0,p.jsx)(u.AW,{location:t,render:()=>e})})}}const M=O,F="__docusaurus-base-url-issue-banner-container",D="__docusaurus-base-url-issue-banner",z="__docusaurus-base-url-issue-banner-suggestion-container";function B(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${F}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n \n\n`}(e)).replace(/{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;o?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var Y=n(66916);const Q=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee={prefetch(e){if(!(e=>!J()&&!X.has(e)&&!Q.has(e))(e))return!1;Q.add(e);const t=(0,d.f)(l.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(W).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,Y.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?K(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!J()&&!X.has(e))(e)&&(X.add(e),R(e))},te=Object.freeze(ee),ne=Boolean(!0);if(i.Z.canUseDOM){window.docusaurus=te;const e=document.getElementById("__docusaurus"),t=(0,p.jsx)(s.B6,{children:(0,p.jsx)(a.VK,{children:(0,p.jsx)(V,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},i=()=>{if(ne)r.startTransition((()=>{o.hydrateRoot(e,t,{onRecoverableError:n})}));else{const a=o.createRoot(e,{onRecoverableError:n});r.startTransition((()=>{a.render(t)}))}};R(window.location.pathname).then(i)}},56725:(e,t,n)=>{"use strict";n.d(t,{_:()=>d,M:()=>p});var r=n(67294),o=n(36809);const a=JSON.parse('{"docusaurus-plugin-google-gtag":{"default":{"trackingID":["G-NZRQD92LEX"],"anonymizeIP":true,"id":"default"}},"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"v5","isLast":true,"path":"/docs","mainDocId":"getting-started/introduction","docs":[{"id":"attributions","path":"/docs/attributions"},{"id":"faq/faq_index","path":"/docs/faq/"},{"id":"flow_diagrams","path":"/docs/flow_diagrams"},{"id":"getting-started/community","path":"/docs/getting-started/community","sidebar":"Introduction"},{"id":"getting-started/comparisons","path":"/docs/getting-started/comparisons","sidebar":"Introduction"},{"id":"getting-started/design","path":"/docs/getting-started/design","sidebar":"Introduction"},{"id":"getting-started/ecosystem","path":"/docs/getting-started/ecosystem","sidebar":"Introduction"},{"id":"getting-started/highlights","path":"/docs/getting-started/highlights","sidebar":"Introduction"},{"id":"getting-started/installation","path":"/docs/getting-started/installation","sidebar":"Introduction"},{"id":"getting-started/integration","path":"/docs/getting-started/integration","sidebar":"Introduction"},{"id":"getting-started/introduction","path":"/docs/getting-started/introduction","sidebar":"Introduction"},{"id":"getting-started/migration_v4","path":"/docs/getting-started/migration_v4"},{"id":"getting-started/migration_v5","path":"/docs/getting-started/migration_v5","sidebar":"Introduction"},{"id":"getting-started/quickstart","path":"/docs/getting-started/quickstart","sidebar":"Introduction"},{"id":"pro/admin_idp_auth","path":"/docs/pro/admin_idp_auth","sidebar":"Pro"},{"id":"pro/analytics","path":"/docs/pro/analytics","sidebar":"Pro"},{"id":"pro/capabilities","path":"/docs/pro/capabilities","sidebar":"Pro"},{"id":"pro/cel_expressions","path":"/docs/pro/cel_expressions","sidebar":"Pro"},{"id":"pro/channel_patterns","path":"/docs/pro/channel_patterns","sidebar":"Pro"},{"id":"pro/channel_state_events","path":"/docs/pro/channel_state_events","sidebar":"Pro"},{"id":"pro/client_message_batching","path":"/docs/pro/client_message_batching","sidebar":"Pro"},{"id":"pro/connections","path":"/docs/pro/connections","sidebar":"Pro"},{"id":"pro/distributed_rate_limit","path":"/docs/pro/distributed_rate_limit"},{"id":"pro/install_and_run","path":"/docs/pro/install_and_run","sidebar":"Pro"},{"id":"pro/observability_enhancements","path":"/docs/pro/observability_enhancements","sidebar":"Pro"},{"id":"pro/overview","path":"/docs/pro/overview","sidebar":"Pro"},{"id":"pro/performance","path":"/docs/pro/performance","sidebar":"Pro"},{"id":"pro/process_stats","path":"/docs/pro/process_stats","sidebar":"Pro"},{"id":"pro/push_notifications","path":"/docs/pro/push_notifications","sidebar":"Pro"},{"id":"pro/rate_limiting","path":"/docs/pro/rate_limiting","sidebar":"Pro"},{"id":"pro/singleflight","path":"/docs/pro/singleflight","sidebar":"Pro"},{"id":"pro/token_revocation","path":"/docs/pro/token_revocation","sidebar":"Pro"},{"id":"pro/tracing","path":"/docs/pro/tracing","sidebar":"Pro"},{"id":"pro/user_block","path":"/docs/pro/user_block","sidebar":"Pro"},{"id":"pro/user_status","path":"/docs/pro/user_status","sidebar":"Pro"},{"id":"server/admin_web","path":"/docs/server/admin_web","sidebar":"Guides"},{"id":"server/authentication","path":"/docs/server/authentication","sidebar":"Guides"},{"id":"server/channel_permissions","path":"/docs/server/channel_permissions","sidebar":"Guides"},{"id":"server/channel_token_auth","path":"/docs/server/channel_token_auth","sidebar":"Guides"},{"id":"server/channels","path":"/docs/server/channels","sidebar":"Guides"},{"id":"server/codes","path":"/docs/server/codes","sidebar":"Guides"},{"id":"server/configuration","path":"/docs/server/configuration","sidebar":"Guides"},{"id":"server/console_commands","path":"/docs/server/console_commands","sidebar":"Guides"},{"id":"server/consumers","path":"/docs/server/consumers","sidebar":"Guides"},{"id":"server/engines","path":"/docs/server/engines","sidebar":"Guides"},{"id":"server/history_and_recovery","path":"/docs/server/history_and_recovery","sidebar":"Guides"},{"id":"server/infra_tuning","path":"/docs/server/infra_tuning","sidebar":"Guides"},{"id":"server/load_balancing","path":"/docs/server/load_balancing","sidebar":"Guides"},{"id":"server/monitoring","path":"/docs/server/monitoring"},{"id":"server/observability","path":"/docs/server/observability","sidebar":"Guides"},{"id":"server/presence","path":"/docs/server/presence","sidebar":"Guides"},{"id":"server/proxy","path":"/docs/server/proxy","sidebar":"Guides"},{"id":"server/proxy_streams","path":"/docs/server/proxy_streams","sidebar":"Guides"},{"id":"server/server_api","path":"/docs/server/server_api","sidebar":"Guides"},{"id":"server/server_subs","path":"/docs/server/server_subs","sidebar":"Guides"},{"id":"server/tls","path":"/docs/server/tls","sidebar":"Guides"},{"id":"transports/client_api","path":"/docs/transports/client_api","sidebar":"Transports"},{"id":"transports/client_protocol","path":"/docs/transports/client_protocol","sidebar":"Transports"},{"id":"transports/client_sdk","path":"/docs/transports/client_sdk","sidebar":"Transports"},{"id":"transports/http_stream","path":"/docs/transports/http_stream","sidebar":"Transports"},{"id":"transports/overview","path":"/docs/transports/overview","sidebar":"Transports"},{"id":"transports/sockjs","path":"/docs/transports/sockjs","sidebar":"Transports"},{"id":"transports/sse","path":"/docs/transports/sse","sidebar":"Transports"},{"id":"transports/uni_client_protocol","path":"/docs/transports/uni_client_protocol","sidebar":"Transports"},{"id":"transports/uni_grpc","path":"/docs/transports/uni_grpc","sidebar":"Transports"},{"id":"transports/uni_http_stream","path":"/docs/transports/uni_http_stream","sidebar":"Transports"},{"id":"transports/uni_sse","path":"/docs/transports/uni_sse","sidebar":"Transports"},{"id":"transports/uni_websocket","path":"/docs/transports/uni_websocket","sidebar":"Transports"},{"id":"transports/websocket","path":"/docs/transports/websocket","sidebar":"Transports"},{"id":"transports/webtransport","path":"/docs/transports/webtransport","sidebar":"Transports"},{"id":"tutorial/backend","path":"/docs/tutorial/backend","sidebar":"Tutorial"},{"id":"tutorial/centrifugo","path":"/docs/tutorial/centrifugo","sidebar":"Tutorial"},{"id":"tutorial/frontend","path":"/docs/tutorial/frontend","sidebar":"Tutorial"},{"id":"tutorial/improvements","path":"/docs/tutorial/improvements","sidebar":"Tutorial"},{"id":"tutorial/intro","path":"/docs/tutorial/intro","sidebar":"Tutorial"},{"id":"tutorial/layout","path":"/docs/tutorial/layout","sidebar":"Tutorial"},{"id":"tutorial/outbox_cdc","path":"/docs/tutorial/outbox_cdc","sidebar":"Tutorial"},{"id":"tutorial/outro","path":"/docs/tutorial/outro","sidebar":"Tutorial"},{"id":"tutorial/recovery","path":"/docs/tutorial/recovery","sidebar":"Tutorial"},{"id":"tutorial/reverse_proxy","path":"/docs/tutorial/reverse_proxy","sidebar":"Tutorial"},{"id":"tutorial/scale","path":"/docs/tutorial/scale","sidebar":"Tutorial"},{"id":"tutorial/tips_and_tricks","path":"/docs/tutorial/tips_and_tricks","sidebar":"Tutorial"}],"draftIds":["pro/tenant_channels"],"sidebars":{"Introduction":{"link":{"path":"/docs/getting-started/introduction","label":"getting-started/introduction"}},"Tutorial":{"link":{"path":"/docs/tutorial/intro","label":"tutorial/intro"}},"Guides":{"link":{"path":"/docs/server/configuration","label":"server/configuration"}},"Transports":{"link":{"path":"/docs/transports/overview","label":"transports/overview"}},"Pro":{"link":{"path":"/docs/pro/overview","label":"pro/overview"}}}},{"name":"4","label":"v4","isLast":false,"path":"/docs/4","mainDocId":"getting-started/introduction","docs":[{"id":"attributions","path":"/docs/4/attributions"},{"id":"faq/faq_index","path":"/docs/4/faq/"},{"id":"flow_diagrams","path":"/docs/4/flow_diagrams"},{"id":"getting-started/client_api","path":"/docs/4/getting-started/client_api"},{"id":"getting-started/community","path":"/docs/4/getting-started/community","sidebar":"Introduction"},{"id":"getting-started/design","path":"/docs/4/getting-started/design","sidebar":"Introduction"},{"id":"getting-started/ecosystem","path":"/docs/4/getting-started/ecosystem","sidebar":"Introduction"},{"id":"getting-started/highlights","path":"/docs/4/getting-started/highlights","sidebar":"Introduction"},{"id":"getting-started/installation","path":"/docs/4/getting-started/installation","sidebar":"Introduction"},{"id":"getting-started/integration","path":"/docs/4/getting-started/integration","sidebar":"Introduction"},{"id":"getting-started/introduction","path":"/docs/4/getting-started/introduction","sidebar":"Introduction"},{"id":"getting-started/migration_v4","path":"/docs/4/getting-started/migration_v4","sidebar":"Introduction"},{"id":"getting-started/quickstart","path":"/docs/4/getting-started/quickstart","sidebar":"Introduction"},{"id":"pro/analytics","path":"/docs/4/pro/analytics","sidebar":"Pro"},{"id":"pro/capabilities","path":"/docs/4/pro/capabilities","sidebar":"Pro"},{"id":"pro/cel_expressions","path":"/docs/4/pro/cel_expressions","sidebar":"Pro"},{"id":"pro/channel_patterns","path":"/docs/4/pro/channel_patterns","sidebar":"Pro"},{"id":"pro/client_message_batching","path":"/docs/4/pro/client_message_batching","sidebar":"Pro"},{"id":"pro/connections","path":"/docs/4/pro/connections","sidebar":"Pro"},{"id":"pro/install_and_run","path":"/docs/4/pro/install_and_run","sidebar":"Pro"},{"id":"pro/overview","path":"/docs/4/pro/overview","sidebar":"Pro"},{"id":"pro/performance","path":"/docs/4/pro/performance","sidebar":"Pro"},{"id":"pro/process_stats","path":"/docs/4/pro/process_stats","sidebar":"Pro"},{"id":"pro/push_notifications","path":"/docs/4/pro/push_notifications","sidebar":"Pro"},{"id":"pro/singleflight","path":"/docs/4/pro/singleflight","sidebar":"Pro"},{"id":"pro/throttling","path":"/docs/4/pro/throttling","sidebar":"Pro"},{"id":"pro/token_revocation","path":"/docs/4/pro/token_revocation","sidebar":"Pro"},{"id":"pro/tracing","path":"/docs/4/pro/tracing","sidebar":"Pro"},{"id":"pro/user_block","path":"/docs/4/pro/user_block","sidebar":"Pro"},{"id":"pro/user_status","path":"/docs/4/pro/user_status","sidebar":"Pro"},{"id":"server/admin_web","path":"/docs/4/server/admin_web","sidebar":"Guides"},{"id":"server/authentication","path":"/docs/4/server/authentication","sidebar":"Guides"},{"id":"server/channel_permissions","path":"/docs/4/server/channel_permissions","sidebar":"Guides"},{"id":"server/channel_token_auth","path":"/docs/4/server/channel_token_auth","sidebar":"Guides"},{"id":"server/channels","path":"/docs/4/server/channels","sidebar":"Guides"},{"id":"server/codes","path":"/docs/4/server/codes","sidebar":"Guides"},{"id":"server/configuration","path":"/docs/4/server/configuration","sidebar":"Guides"},{"id":"server/console_commands","path":"/docs/4/server/console_commands","sidebar":"Guides"},{"id":"server/engines","path":"/docs/4/server/engines","sidebar":"Guides"},{"id":"server/history_and_recovery","path":"/docs/4/server/history_and_recovery","sidebar":"Guides"},{"id":"server/infra_tuning","path":"/docs/4/server/infra_tuning","sidebar":"Guides"},{"id":"server/load_balancing","path":"/docs/4/server/load_balancing","sidebar":"Guides"},{"id":"server/monitoring","path":"/docs/4/server/monitoring","sidebar":"Guides"},{"id":"server/presence","path":"/docs/4/server/presence","sidebar":"Guides"},{"id":"server/proxy","path":"/docs/4/server/proxy","sidebar":"Guides"},{"id":"server/server_api","path":"/docs/4/server/server_api","sidebar":"Guides"},{"id":"server/server_subs","path":"/docs/4/server/server_subs","sidebar":"Guides"},{"id":"server/tls","path":"/docs/4/server/tls","sidebar":"Guides"},{"id":"transports/client_api","path":"/docs/4/transports/client_api","sidebar":"Transports"},{"id":"transports/client_protocol","path":"/docs/4/transports/client_protocol","sidebar":"Transports"},{"id":"transports/client_sdk","path":"/docs/4/transports/client_sdk","sidebar":"Transports"},{"id":"transports/http_stream","path":"/docs/4/transports/http_stream","sidebar":"Transports"},{"id":"transports/overview","path":"/docs/4/transports/overview","sidebar":"Transports"},{"id":"transports/sockjs","path":"/docs/4/transports/sockjs","sidebar":"Transports"},{"id":"transports/sse","path":"/docs/4/transports/sse","sidebar":"Transports"},{"id":"transports/uni_grpc","path":"/docs/4/transports/uni_grpc","sidebar":"Transports"},{"id":"transports/uni_http_stream","path":"/docs/4/transports/uni_http_stream","sidebar":"Transports"},{"id":"transports/uni_sse","path":"/docs/4/transports/uni_sse","sidebar":"Transports"},{"id":"transports/uni_websocket","path":"/docs/4/transports/uni_websocket","sidebar":"Transports"},{"id":"transports/websocket","path":"/docs/4/transports/websocket","sidebar":"Transports"},{"id":"transports/webtransport","path":"/docs/4/transports/webtransport","sidebar":"Transports"}],"draftIds":["pro/tenant_channels"],"sidebars":{"Introduction":{"link":{"path":"/docs/4/getting-started/introduction","label":"getting-started/introduction"}},"Guides":{"link":{"path":"/docs/4/server/configuration","label":"server/configuration"}},"Transports":{"link":{"path":"/docs/4/transports/overview","label":"transports/overview"}},"Pro":{"link":{"path":"/docs/4/pro/overview","label":"pro/overview"}}}},{"name":"3","label":"v3","isLast":false,"path":"/docs/3","mainDocId":"getting-started/introduction","docs":[{"id":"attributions","path":"/docs/3/attributions"},{"id":"ecosystem/centrifuge","path":"/docs/3/ecosystem/centrifuge","sidebar":"Ecosystem"},{"id":"ecosystem/integrations","path":"/docs/3/ecosystem/integrations","sidebar":"Ecosystem"},{"id":"faq/faq_index","path":"/docs/3/faq/"},{"id":"flow_diagrams","path":"/docs/3/flow_diagrams"},{"id":"getting-started/client_api","path":"/docs/3/getting-started/client_api","sidebar":"Introduction"},{"id":"getting-started/design","path":"/docs/3/getting-started/design","sidebar":"Introduction"},{"id":"getting-started/highlights","path":"/docs/3/getting-started/highlights","sidebar":"Introduction"},{"id":"getting-started/installation","path":"/docs/3/getting-started/installation","sidebar":"Introduction"},{"id":"getting-started/integration","path":"/docs/3/getting-started/integration","sidebar":"Introduction"},{"id":"getting-started/introduction","path":"/docs/3/getting-started/introduction","sidebar":"Introduction"},{"id":"getting-started/migration_v3","path":"/docs/3/getting-started/migration_v3","sidebar":"Introduction"},{"id":"getting-started/quickstart","path":"/docs/3/getting-started/quickstart","sidebar":"Introduction"},{"id":"pro/analytics","path":"/docs/3/pro/analytics","sidebar":"Pro"},{"id":"pro/db_namespaces","path":"/docs/3/pro/db_namespaces"},{"id":"pro/install_and_run","path":"/docs/3/pro/install_and_run","sidebar":"Pro"},{"id":"pro/overview","path":"/docs/3/pro/overview","sidebar":"Pro"},{"id":"pro/performance","path":"/docs/3/pro/performance","sidebar":"Pro"},{"id":"pro/process_stats","path":"/docs/3/pro/process_stats","sidebar":"Pro"},{"id":"pro/singleflight","path":"/docs/3/pro/singleflight","sidebar":"Pro"},{"id":"pro/throttling","path":"/docs/3/pro/throttling","sidebar":"Pro"},{"id":"pro/token_revocation","path":"/docs/3/pro/token_revocation","sidebar":"Pro"},{"id":"pro/tracing","path":"/docs/3/pro/tracing","sidebar":"Pro"},{"id":"pro/user_block","path":"/docs/3/pro/user_block","sidebar":"Pro"},{"id":"pro/user_connections","path":"/docs/3/pro/user_connections","sidebar":"Pro"},{"id":"pro/user_status","path":"/docs/3/pro/user_status","sidebar":"Pro"},{"id":"server/admin_web","path":"/docs/3/server/admin_web","sidebar":"Guides"},{"id":"server/authentication","path":"/docs/3/server/authentication","sidebar":"Guides"},{"id":"server/channels","path":"/docs/3/server/channels","sidebar":"Guides"},{"id":"server/codes","path":"/docs/3/server/codes","sidebar":"Guides"},{"id":"server/configuration","path":"/docs/3/server/configuration","sidebar":"Guides"},{"id":"server/console_commands","path":"/docs/3/server/console_commands","sidebar":"Guides"},{"id":"server/engines","path":"/docs/3/server/engines","sidebar":"Guides"},{"id":"server/history_and_recovery","path":"/docs/3/server/history_and_recovery","sidebar":"Guides"},{"id":"server/infra_tuning","path":"/docs/3/server/infra_tuning","sidebar":"Guides"},{"id":"server/load_balancing","path":"/docs/3/server/load_balancing","sidebar":"Guides"},{"id":"server/monitoring","path":"/docs/3/server/monitoring","sidebar":"Guides"},{"id":"server/private_channels","path":"/docs/3/server/private_channels","sidebar":"Guides"},{"id":"server/proxy","path":"/docs/3/server/proxy","sidebar":"Guides"},{"id":"server/server_api","path":"/docs/3/server/server_api","sidebar":"Guides"},{"id":"server/server_subs","path":"/docs/3/server/server_subs","sidebar":"Guides"},{"id":"server/tls","path":"/docs/3/server/tls","sidebar":"Guides"},{"id":"transports/client_protocol","path":"/docs/3/transports/client_protocol","sidebar":"Transports"},{"id":"transports/client_sdk","path":"/docs/3/transports/client_sdk","sidebar":"Transports"},{"id":"transports/overview","path":"/docs/3/transports/overview","sidebar":"Transports"},{"id":"transports/sockjs","path":"/docs/3/transports/sockjs","sidebar":"Transports"},{"id":"transports/uni_grpc","path":"/docs/3/transports/uni_grpc","sidebar":"Transports"},{"id":"transports/uni_http_stream","path":"/docs/3/transports/uni_http_stream","sidebar":"Transports"},{"id":"transports/uni_sse","path":"/docs/3/transports/uni_sse","sidebar":"Transports"},{"id":"transports/uni_websocket","path":"/docs/3/transports/uni_websocket","sidebar":"Transports"},{"id":"transports/websocket","path":"/docs/3/transports/websocket","sidebar":"Transports"}],"draftIds":[],"sidebars":{"Introduction":{"link":{"path":"/docs/3/getting-started/introduction","label":"getting-started/introduction"}},"Guides":{"link":{"path":"/docs/3/server/configuration","label":"server/configuration"}},"Transports":{"link":{"path":"/docs/3/transports/overview","label":"transports/overview"}},"Pro":{"link":{"path":"/docs/3/pro/overview","label":"pro/overview"}},"Ecosystem":{"link":{"path":"/docs/3/ecosystem/centrifuge","label":"ecosystem/centrifuge"}}}}],"breadcrumbs":false}}}'),s=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var i=n(57529);const c=JSON.parse('{"docusaurusVersion":"3.1.0","siteVersion":"1.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.1.0"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.1.0"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.1.0"},"docusaurus-plugin-google-gtag":{"type":"package","name":"@docusaurus/plugin-google-gtag","version":"3.1.0"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.1.0"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.1.0"},"docusaurus-theme-search-algolia":{"type":"package","name":"@docusaurus/theme-search-algolia","version":"3.1.0"}}}');var l=n(85893);const u={siteConfig:o.default,siteMetadata:c,globalData:a,i18n:s,codeTranslations:i},d=r.createContext(u);function p(e){let{children:t}=e;return(0,l.jsx)(d.Provider,{value:u,children:t})}},24649:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(67294),o=n(19901),a=n(32411),s=n(79861),i=n(78299),c=n(85893);function l(e){let{error:t,tryAgain:n}=e;return(0,c.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,c.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,c.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,c.jsx)(u,{error:t})]})}function u(e){let{error:t}=e;const n=(0,s.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,c.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function d(e){let{error:t,tryAgain:n}=e;return(0,c.jsxs)(f,{fallback:()=>(0,c.jsx)(l,{error:t,tryAgain:n}),children:[(0,c.jsx)(a.Z,{children:(0,c.jsx)("title",{children:"Page Error"})}),(0,c.jsx)(i.Z,{children:(0,c.jsx)(l,{error:t,tryAgain:n})})]})}const p=e=>(0,c.jsx)(d,{...e});class f extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){o.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??p)(e)}return e??null}}},19901:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,o={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},32411:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(67294);var r=n(70405),o=n(85893);function a(e){return(0,o.jsx)(r.ql,{...e})}},75013:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(67294),o=n(73727),a=n(79861),s=n(6832),i=n(71699),c=n(19901),l=n(43444),u=n(51402),d=n(85893);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:g,isActive:h,"data-noBrokenLinkCheck":m,autoAddBaseUrl:b=!0,...v}=e;const{siteConfig:{trailingSlash:y,baseUrl:_}}=(0,s.Z)(),{withBaseUrl:w}=(0,u.C)(),x=(0,l.Z)(),k=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>k.current));const S=p||f;const E=(0,i.Z)(S),C=S?.replace("pathname://","");let T=void 0!==C?(P=C,b&&(e=>e.startsWith("/"))(P)?w(P):P):void 0;var P;T&&E&&(T=(0,a.applyTrailingSlash)(T,{trailingSlash:y,baseUrl:_}));const j=(0,r.useRef)(!1),A=n?o.OL:o.rU,L=c.Z.canUseIntersectionObserver,N=(0,r.useRef)(),I=()=>{j.current||null==T||(window.docusaurus.preload(T),j.current=!0)};(0,r.useEffect)((()=>(!L&&E&&null!=T&&window.docusaurus.prefetch(T),()=>{L&&N.current&&N.current.disconnect()})),[N,T,L,E]);const R=T?.startsWith("#")??!1,O=!T||!E||R;return O||m||x.collectLink(T),O?(0,d.jsx)("a",{ref:k,href:T,...S&&!E&&{target:"_blank",rel:"noopener noreferrer"},...v}):(0,d.jsx)(A,{...v,onMouseEnter:I,onTouchStart:I,innerRef:e=>{k.current=e,L&&e&&E&&(N.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(N.current.unobserve(e),N.current.disconnect(),null!=T&&window.docusaurus.prefetch(T))}))})),N.current.observe(e))},to:T,...n&&{isActive:h,activeClassName:g}})}const f=r.forwardRef(p)},11614:(e,t,n)=>{"use strict";n.d(t,{Z:()=>l,I:()=>c});var r=n(67294),o=n(85893);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var s=n(57529);function i(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return s[t??n]??n??t}function c(e,t){let{message:n,id:r}=e;return a(i({message:n,id:r}),t)}function l(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("IllegalYour Docusaurus site did not load properly.
\nA very common reason is a wrong site baseUrl configuration.
\nCurrent configured baseUrl = ${e} ${"/"===e?" (default value)":""}
\nWe suggest trying baseUrl =
\nchildren",t),new Error("The Docusaurus component only accept simple string values");const s=i({message:t,id:n});return(0,o.jsx)(o.Fragment,{children:a(s,r)})}},12497:(e,t,n)=>{"use strict";n.d(t,{m:()=>r});const r="default"},71699:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function o(e){return void 0!==e&&!r(e)}n.d(t,{Z:()=>o,b:()=>r})},51402:(e,t,n)=>{"use strict";n.d(t,{C:()=>s,Z:()=>i});var r=n(67294),o=n(6832),a=n(71699);function s(){const{siteConfig:{baseUrl:e,url:t}}=(0,o.Z)(),n=(0,r.useCallback)(((n,r)=>function(e,t,n,r){let{forcePrependBaseUrl:o=!1,absolute:s=!1}=void 0===r?{}:r;if(!n||n.startsWith("#")||(0,a.b)(n))return n;if(o)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const i=n.startsWith(t)?n:t+n.replace(/^\//,"");return s?e+i:i}(t,e,n,r)),[t,e]);return{withBaseUrl:n}}function i(e,t){void 0===t&&(t={});const{withBaseUrl:n}=s();return n(e,t)}},43444:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(67294);n(85893);const o=r.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,r.useContext)(o);function s(){return a()}},6832:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(67294),o=n(56725);function a(){return(0,r.useContext)(o._)}},5730:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(67294),o=n(74058);function a(){return(0,r.useContext)(o._)}},20613:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(67294);const o=n(19901).Z.canUseDOM?r.useLayoutEffect:r.useEffect},66916:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function o(e){const t={};return function e(n,o){Object.entries(n).forEach((n=>{let[a,s]=n;const i=o?`${o}.${a}`:a;r(s)?e(s,i):t[i]=s}))}(e),t}},66041:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,z:()=>s});var r=n(67294),o=n(85893);const a=r.createContext(null);function s(e){let{children:t,value:n}=e;const s=r.useContext(a),i=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:s,value:n})),[s,n]);return(0,o.jsx)(a.Provider,{value:i,children:t})}},4452:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>b,gA:()=>f,WS:()=>g,_r:()=>d,Jo:()=>v,zh:()=>p,yW:()=>m,gB:()=>h});var r=n(16550),o=n(6832),a=n(12497);function s(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,o.Z)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const i=e=>e.versions.find((e=>e.isLast));function c(e,t){const n=i(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,r.LX)(t,{path:e.path,exact:!1,strict:!1})))}function l(e,t){const n=c(e,t),o=n?.docs.find((e=>!!(0,r.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:o,alternateDocVersions:o?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(o.id):{}}}const u={},d=()=>s("docusaurus-plugin-content-docs")??u,p=e=>function(e,t,n){void 0===t&&(t=a.m),void 0===n&&(n={});const r=s(e),o=r?.[t];if(!o&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return o}("docusaurus-plugin-content-docs",e,{failfast:!0});function f(e){void 0===e&&(e={});const t=d(),{pathname:n}=(0,r.TH)();return function(e,t,n){void 0===n&&(n={});const o=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.LX)(t,{path:n.path,exact:!1,strict:!1})})),a=o?{pluginId:o[0],pluginData:o[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function g(e){void 0===e&&(e={});const t=f(e),{pathname:n}=(0,r.TH)();if(!t)return;return{activePlugin:t,activeVersion:c(t.pluginData,n)}}function h(e){return p(e).versions}function m(e){const t=p(e);return i(t)}function b(e){const t=p(e),{pathname:n}=(0,r.TH)();return l(t,n)}function v(e){const t=p(e),{pathname:n}=(0,r.TH)();return function(e,t){const n=i(e);return{latestDocSuggestion:l(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},25557:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={onRouteDidUpdate(e){let{location:t,previousLocation:n}=e;!n||t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash||setTimeout((()=>{window.gtag("set","page_path",t.pathname+t.search+t.hash),window.gtag("event","page_view")}))}}},26126:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(74865),o=n.n(r);o().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{o().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){o().done()}}},25529:(e,t,n)=>{"use strict";n.r(t);var r=n(14965),o=n(36809);!function(e){const{themeConfig:{prism:t}}=o.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(96854),n(30977)(`./prism-${e}`)})),delete globalThis.Prism}(r.p1)},34055:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(67294);var r=n(36905),o=n(11614),a=n(96793),s=n(75013),i=n(43444);const c={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var l=n(85893);function u(e){let{as:t,id:n,...u}=e;const d=(0,i.Z)(),{navbar:{hideOnScroll:p}}=(0,a.L)();if("h1"===t||!n)return(0,l.jsx)(t,{...u,id:void 0});d.collectAnchor(n);const f=(0,o.I)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof u.children?u.children:n});return(0,l.jsxs)(t,{...u,className:(0,r.Z)("anchor",p?c.anchorWithHideOnScrollNavbar:c.anchorWithStickyNavbar,u.className),id:n,children:[u.children,(0,l.jsx)(s.Z,{className:"hash-link",to:`#${n}`,"aria-label":f,title:f,children:"\u200b"})]})}},43399:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(67294);const r={iconExternalLink:"iconExternalLink_nPIU"};var o=n(85893);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,o.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,o.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},78299:(e,t,n)=>{"use strict";n.d(t,{Z:()=>Nt});var r=n(67294),o=n(36905),a=n(24649),s=n(44873),i=n(16550),c=n(11614),l=n(68265),u=n(85893);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,i.k6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,l.S)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const g=(0,c.I)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??g,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":g,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var m=n(18015),b=n(22768);const v={skipToContent:"skipToContent_fXgn"};function y(){return(0,u.jsx)(h,{className:v.skipToContent})}var _=n(96793),w=n(69061);function x(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:a,...s}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...s,children:(0,u.jsx)("g",{stroke:r,strokeWidth:o,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const k={closeButton:"closeButton_CVFx"};function S(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,c.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,o.Z)("clean-btn close",k.closeButton,e.className),children:(0,u.jsx)(x,{width:14,height:14,strokeWidth:3.1})})}const E={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,_.L)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,o.Z)(E.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function P(){const{announcementBar:e}=(0,_.L)(),{isActive:t,close:n}=(0,w.nT)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:a}=e;return(0,u.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:r,color:o},role:"banner",children:[a&&(0,u.jsx)("div",{className:T.announcementBarPlaceholder}),(0,u.jsx)(C,{className:T.announcementBarContent}),a&&(0,u.jsx)(S,{onClick:n,className:T.announcementBarClose})]})}var j=n(35022),A=n(63735);var L=n(93478),N=n(82306);const I=r.createContext(null);function R(e){let{children:t}=e;const n=function(){const e=(0,j.e)(),t=(0,N.HY)(),[n,o]=(0,r.useState)(!1),a=null!==t.component,s=(0,L.D9)(a);return(0,r.useEffect)((()=>{a&&!s&&o(!0)}),[a,s]),(0,r.useEffect)((()=>{a?e.shown||o(!0):o(!1)}),[e.shown,a]),(0,r.useMemo)((()=>[n,o]),[n])}();return(0,u.jsx)(I.Provider,{value:n,children:t})}function O(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function M(){const e=(0,r.useContext)(I);if(!e)throw new L.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,o=(0,r.useCallback)((()=>n(!1)),[n]),a=(0,N.HY)();return(0,r.useMemo)((()=>({shown:t,hide:o,content:O(a)})),[o,a,t])}function F(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:a}=M();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,o.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var D=n(70524),z=n(5730);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function Z(e){let{className:t,buttonClassName:n,value:r,onChange:a}=e;const s=(0,z.Z)(),i=(0,c.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,c.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,c.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,o.Z)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,o.Z)("clean-btn",U.toggleButton,!s&&U.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===r?"light":"dark"),disabled:!s,title:i,"aria-label":i,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,o.Z)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,o.Z)(U.toggleIcon,U.darkToggleIcon)})]})})}const H=r.memo(Z),G={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function q(e){let{className:t}=e;const n=(0,_.L)().navbar.style,r=(0,_.L)().colorMode.disableSwitch,{colorMode:o,setColorMode:a}=(0,D.I)();return r?null:(0,u.jsx)(H,{className:t,buttonClassName:"dark"===n?G.darkNavbarColorModeToggle:void 0,value:o,onChange:a})}var V=n(49627);function W(){return(0,u.jsx)(V.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function K(){const e=(0,j.e)();return(0,u.jsx)("button",{type:"button","aria-label":(0,c.I)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(x,{color:"var(--ifm-color-emphasis-600)"})})}function Y(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(W,{}),(0,u.jsx)(q,{className:"margin-right--md"}),(0,u.jsx)(K,{})]})}var Q=n(75013),X=n(51402),J=n(71699),ee=n(88648),te=n(43399);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:a,html:s,isDropdownLink:i,prependBaseUrlToHref:c,...l}=e;const d=(0,X.Z)(r),p=(0,X.Z)(t),f=(0,X.Z)(o,{forcePrependBaseUrl:!0}),g=a&&o&&!(0,J.Z)(o),h=s?{dangerouslySetInnerHTML:{__html:s}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,g&&(0,u.jsx)(te.Z,{...i&&{width:12,height:12}})]})};return o?(0,u.jsx)(Q.Z,{href:c?f:o,...l,...h}):(0,u.jsx)(Q.Z,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?(0,ee.F)(n,t.pathname):t.pathname.startsWith(p)},...l,...h})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const a=(0,u.jsx)(ne,{className:(0,o.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:a}):a}function oe(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,o.Z)("menu__link",t),...r})})}function ae(e){let{mobile:t=!1,position:n,...r}=e;const o=t?oe:re;return(0,u.jsx)(o,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var se=n(17940),ie=n(18407),ce=n(6832);const le={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,ie.Mg)(e.to,t)||!!(0,ee.F)(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:a,onClick:s,...i}=e;const c=(0,r.useRef)(null),[l,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{c.current&&!c.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[c]),(0,u.jsxs)("div",{ref:c,className:(0,o.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":l}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":l,role:"button",href:i.to?void 0:"#",className:(0,o.Z)("navbar__link",a),...i,onClick:i.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!l))},children:i.children??i.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(He,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:s,...c}=e;const l=function(){const{siteConfig:{baseUrl:e}}=(0,ce.Z)(),{pathname:t}=(0,i.TH)();return t.replace(e,"/")}(),d=ue(t,l),{collapsed:p,toggleCollapsed:f,setCollapsed:g}=(0,se.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&g(!d)}),[l,d,g]),(0,u.jsxs)("li",{className:(0,o.Z)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,o.Z)(le.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...c,onClick:e=>{e.preventDefault(),f()},children:c.children??c.label}),(0,u.jsx)(se.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(He,{mobile:!0,isDropdownItem:!0,onClick:s,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var ge=n(13156);function he(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const me="iconLanguage_nlXk";var be=n(73935);function ve(){return r.createElement("svg",{width:"15",height:"15",className:"DocSearch-Control-Key-Icon"},r.createElement("path",{d:"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953",strokeWidth:"1.2",stroke:"currentColor",fill:"none",strokeLinecap:"square"}))}var ye=n(20830),_e=["translations"];function we(){return we=Object.assign||function(e){for(var t=1;t e.length)&&(t=e.length);for(var n=0,r=new Array(t);n =0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r =0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var Ee="Ctrl";var Ce=r.forwardRef((function(e,t){var n=e.translations,o=void 0===n?{}:n,a=Se(e,_e),s=o.buttonText,i=void 0===s?"Search":s,c=o.buttonAriaLabel,l=void 0===c?"Search":c,u=xe((0,r.useState)(null),2),d=u[0],p=u[1];return(0,r.useEffect)((function(){"undefined"!=typeof navigator&&(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?p("\u2318"):p(Ee))}),[]),r.createElement("button",we({type:"button",className:"DocSearch DocSearch-Button","aria-label":l},a,{ref:t}),r.createElement("span",{className:"DocSearch-Button-Container"},r.createElement(ye.W,null),r.createElement("span",{className:"DocSearch-Button-Placeholder"},i)),r.createElement("span",{className:"DocSearch-Button-Keys"},null!==d&&r.createElement(r.Fragment,null,r.createElement("kbd",{className:"DocSearch-Button-Key"},d===Ee?r.createElement(ve,null):d),r.createElement("kbd",{className:"DocSearch-Button-Key"},"K"))))})),Te=n(32411),Pe=n(9512),je=n(80180),Ae=n(39105);const Le={button:{buttonText:(0,c.I)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"}),buttonAriaLabel:(0,c.I)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"})},modal:{searchBox:{resetButtonTitle:(0,c.I)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),resetButtonAriaLabel:(0,c.I)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),cancelButtonText:(0,c.I)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"}),cancelButtonAriaLabel:(0,c.I)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"})},startScreen:{recentSearchesTitle:(0,c.I)({id:"theme.SearchModal.startScreen.recentSearchesTitle",message:"Recent",description:"The title for recent searches"}),noRecentSearchesText:(0,c.I)({id:"theme.SearchModal.startScreen.noRecentSearchesText",message:"No recent searches",description:"The text when no recent searches"}),saveRecentSearchButtonTitle:(0,c.I)({id:"theme.SearchModal.startScreen.saveRecentSearchButtonTitle",message:"Save this search",description:"The label for save recent search button"}),removeRecentSearchButtonTitle:(0,c.I)({id:"theme.SearchModal.startScreen.removeRecentSearchButtonTitle",message:"Remove this search from history",description:"The label for remove recent search button"}),favoriteSearchesTitle:(0,c.I)({id:"theme.SearchModal.startScreen.favoriteSearchesTitle",message:"Favorite",description:"The title for favorite searches"}),removeFavoriteSearchButtonTitle:(0,c.I)({id:"theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle",message:"Remove this search from favorites",description:"The label for remove favorite search button"})},errorScreen:{titleText:(0,c.I)({id:"theme.SearchModal.errorScreen.titleText",message:"Unable to fetch results",description:"The title for error screen of search modal"}),helpText:(0,c.I)({id:"theme.SearchModal.errorScreen.helpText",message:"You might want to check your network connection.",description:"The help text for error screen of search modal"})},footer:{selectText:(0,c.I)({id:"theme.SearchModal.footer.selectText",message:"to select",description:"The explanatory text of the action for the enter key"}),selectKeyAriaLabel:(0,c.I)({id:"theme.SearchModal.footer.selectKeyAriaLabel",message:"Enter key",description:"The ARIA label for the Enter key button that makes the selection"}),navigateText:(0,c.I)({id:"theme.SearchModal.footer.navigateText",message:"to navigate",description:"The explanatory text of the action for the Arrow up and Arrow down key"}),navigateUpKeyAriaLabel:(0,c.I)({id:"theme.SearchModal.footer.navigateUpKeyAriaLabel",message:"Arrow up",description:"The ARIA label for the Arrow up key button that makes the navigation"}),navigateDownKeyAriaLabel:(0,c.I)({id:"theme.SearchModal.footer.navigateDownKeyAriaLabel",message:"Arrow down",description:"The ARIA label for the Arrow down key button that makes the navigation"}),closeText:(0,c.I)({id:"theme.SearchModal.footer.closeText",message:"to close",description:"The explanatory text of the action for Escape key"}),closeKeyAriaLabel:(0,c.I)({id:"theme.SearchModal.footer.closeKeyAriaLabel",message:"Escape key",description:"The ARIA label for the Escape key button that close the modal"}),searchByText:(0,c.I)({id:"theme.SearchModal.footer.searchByText",message:"Search by",description:"The text explain that the search is making by Algolia"})},noResultsScreen:{noResultsText:(0,c.I)({id:"theme.SearchModal.noResultsScreen.noResultsText",message:"No results for",description:"The text explains that there are no results for the following search"}),suggestedQueryText:(0,c.I)({id:"theme.SearchModal.noResultsScreen.suggestedQueryText",message:"Try searching for",description:"The text for the suggested query when no results are found for the following search"}),reportMissingResultsText:(0,c.I)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsText",message:"Believe this query should return results?",description:"The text for the question where the user thinks there are missing results"}),reportMissingResultsLinkText:(0,c.I)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText",message:"Let us know.",description:"The text for the link to report missing results"})}},placeholder:(0,c.I)({id:"theme.SearchModal.placeholder",message:"Search docs",description:"The placeholder of the input of the DocSearch pop-up modal"})};let Ne=null;function Ie(e){let{hit:t,children:n}=e;return(0,u.jsx)(Q.Z,{to:t.url,children:n})}function Re(e){let{state:t,onClose:n}=e;const r=(0,Pe.M)();return(0,u.jsx)(Q.Z,{to:r(t.query),onClick:n,children:(0,u.jsx)(c.Z,{id:"theme.SearchBar.seeAll",values:{count:t.context.nbHits},children:"See all {count} results"})})}function Oe(e){let{contextualSearch:t,externalUrlRegex:o,...a}=e;const{siteMetadata:s}=(0,ce.Z)(),c=(0,je.l)(),l=function(){const{locale:e,tags:t}=(0,Ae._q)();return[`language:${e}`,t.map((e=>`docusaurus_tag:${e}`))]}(),d=a.searchParameters?.facetFilters??[],p=t?function(e,t){const n=e=>"string"==typeof e?[e]:e;return[...n(e),...n(t)]}(l,d):d,f={...a.searchParameters,facetFilters:p},g=(0,i.k6)(),h=(0,r.useRef)(null),m=(0,r.useRef)(null),[b,v]=(0,r.useState)(!1),[y,_]=(0,r.useState)(void 0),w=(0,r.useCallback)((()=>Ne?Promise.resolve():Promise.all([n.e(1426).then(n.bind(n,56672)),Promise.all([n.e(3312),n.e(6945)]).then(n.bind(n,46945)),Promise.all([n.e(3312),n.e(8894)]).then(n.bind(n,18894))]).then((e=>{let[{DocSearchModal:t}]=e;Ne=t}))),[]),x=(0,r.useCallback)((()=>{w().then((()=>{h.current=document.createElement("div"),document.body.insertBefore(h.current,document.body.firstChild),v(!0)}))}),[w,v]),k=(0,r.useCallback)((()=>{v(!1),h.current?.remove()}),[v]),S=(0,r.useCallback)((e=>{w().then((()=>{v(!0),_(e.key)}))}),[w,v,_]),E=(0,r.useRef)({navigate(e){let{itemUrl:t}=e;(0,ee.F)(o,t)?window.location.href=t:g.push(t)}}).current,C=(0,r.useRef)((e=>a.transformItems?a.transformItems(e):e.map((e=>({...e,url:c(e.url)}))))).current,T=(0,r.useMemo)((()=>e=>(0,u.jsx)(Re,{...e,onClose:k})),[k]),P=(0,r.useCallback)((e=>(e.addAlgoliaAgent("docusaurus",s.docusaurusVersion),e)),[s.docusaurusVersion]);return function(e){var t=e.isOpen,n=e.onOpen,o=e.onClose,a=e.onInput,s=e.searchButtonRef;r.useEffect((function(){function e(e){var r;(27===e.keyCode&&t||"k"===(null===(r=e.key)||void 0===r?void 0:r.toLowerCase())&&(e.metaKey||e.ctrlKey)||!function(e){var t=e.target,n=t.tagName;return t.isContentEditable||"INPUT"===n||"SELECT"===n||"TEXTAREA"===n}(e)&&"/"===e.key&&!t)&&(e.preventDefault(),t?o():document.body.classList.contains("DocSearch--active")||document.body.classList.contains("DocSearch--active")||n()),s&&s.current===document.activeElement&&a&&/[a-zA-Z0-9]/.test(String.fromCharCode(e.keyCode))&&a(e)}return window.addEventListener("keydown",e),function(){window.removeEventListener("keydown",e)}}),[t,n,o,a,s])}({isOpen:b,onOpen:x,onClose:k,onInput:S,searchButtonRef:m}),(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(Te.Z,{children:(0,u.jsx)("link",{rel:"preconnect",href:`https://${a.appId}-dsn.algolia.net`,crossOrigin:"anonymous"})}),(0,u.jsx)(Ce,{onTouchStart:w,onFocus:w,onMouseOver:w,onClick:x,ref:m,translations:Le.button}),b&&Ne&&h.current&&(0,be.createPortal)((0,u.jsx)(Ne,{onClose:k,initialScrollY:window.scrollY,initialQuery:y,navigator:E,transformItems:C,hitComponent:Ie,transformSearchClient:P,...a.searchPagePath&&{resultsFooterComponent:T},...a,searchParameters:f,placeholder:Le.placeholder,translations:Le.modal}),h.current)]})}function Me(){const{siteConfig:e}=(0,ce.Z)();return(0,u.jsx)(Oe,{...e.themeConfig.algolia})}const Fe={navbarSearchContainer:"navbarSearchContainer_Bca1"};function De(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,o.Z)(n,Fe.navbarSearchContainer),children:t})}var ze=n(4452),Be=n(85919);var $e=n(4049);const Ue=e=>e.docs.find((t=>t.id===e.mainDocId));const Ze={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:o="",...a}=e;const{i18n:{currentLocale:s,locales:l,localeConfigs:d}}=(0,ce.Z)(),p=(0,ge.l)(),{search:f,hash:g}=(0,i.TH)(),h=[...n,...l.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${g}${o}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===s?t?"menu__link--active":"dropdown__link--active":""}})),...r],m=t?(0,c.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[s].label;return(0,u.jsx)(fe,{...a,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(he,{className:me}),m]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(De,{className:n,children:(0,u.jsx)(Me,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:a=!1}=e;const s=a?"li":"div";return(0,u.jsx)(s,{className:(0,o.Z)({navbar__item:!r&&!a,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,ze.Iw)(r),s=(0,Be.vY)(t,r),i=a?.path===s?.path;return null===s||s.unlisted&&!i?null:(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>i||!!a?.sidebar&&a.sidebar===s.sidebar,label:n??s.id,to:s.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,ze.Iw)(r),s=(0,Be.oz)(t,r).link;if(!s)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>a?.sidebar===t,label:n??s.label,to:s.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const a=(0,Be.lO)(r)[0],s=t??a.label,i=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,u.jsx)(ae,{...o,label:s,to:i})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:a,...s}=e;const{search:l,hash:d}=(0,i.TH)(),p=(0,ze.Iw)(n),f=(0,ze.gB)(n),{savePreferredVersionName:g}=(0,$e.J)(n),h=[...o,...f.map((e=>{const t=p.alternateDocVersions[e.name]??Ue(e);return{label:e.label,to:`${t.path}${l}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>g(e.name)}})),...a],m=(0,Be.lO)(n)[0],b=t&&h.length>1?(0,c.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):m.label,v=t&&h.length>1?void 0:Ue(m).path;return h.length<=1?(0,u.jsx)(ae,{...s,mobile:t,label:b,to:v,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...s,mobile:t,label:b,to:v,items:h,isActive:r?()=>!1:void 0})}};function He(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=Ze[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(o,{...n})}function Ge(){const e=(0,j.e)(),t=(0,_.L)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(He,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function qe(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(c.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function Ve(){const e=0===(0,_.L)().navbar.items.length,t=M();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(qe,{onClick:()=>t.hide()}),t.content]})}function We(){const e=(0,j.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(F,{header:(0,u.jsx)(Y,{}),primaryMenu:(0,u.jsx)(Ge,{}),secondaryMenu:(0,u.jsx)(Ve,{})}):null}const Ke={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ye(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,o.Z)("navbar-sidebar__backdrop",e.className)})}function Qe(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,_.L)(),s=(0,j.e)(),{navbarRef:i,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),o=(0,r.useRef)(!1),a=(0,r.useRef)(0),s=(0,r.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,A.RF)(((t,r)=>{let{scrollY:s}=t;if(!e)return;if(s =i?n(!1):s+l {if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return o.current=!0,void n(!1);n(!0)})),{navbarRef:s,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:i,"aria-label":(0,c.I)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,o.Z)("navbar","navbar--fixed-top",n&&[Ke.navbarHideable,!d&&Ke.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":s.shown}),children:[t,(0,u.jsx)(Ye,{onClick:s.toggle}),(0,u.jsx)(We,{})]})}var Xe=n(79861);const Je={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function et(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(c.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function tt(e){let{error:t}=e;const n=(0,Xe.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Je.errorBoundaryError,children:n})}class nt extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const rt="right";function ot(e){let{width:t=30,height:n=30,className:r,...o}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...o,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function at(){const{toggle:e,shown:t}=(0,j.e)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,c.I)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(ot,{})})}const st={colorModeToggle:"colorModeToggle_DEke"};function it(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)(nt,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(He,{...e})},t)))})}function ct(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function lt(){const e=(0,j.e)(),t=(0,_.L)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??rt)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return(0,u.jsx)(ct,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(at,{}),(0,u.jsx)(W,{}),(0,u.jsx)(it,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(it,{items:r}),(0,u.jsx)(q,{className:st.colorModeToggle}),!o&&(0,u.jsx)(De,{children:(0,u.jsx)(Me,{})})]})})}function ut(){return(0,u.jsx)(Qe,{children:(0,u.jsx)(lt,{})})}function dt(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:a,...s}=t,i=(0,X.Z)(n),c=(0,X.Z)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(Q.Z,{className:"footer__link-item",...r?{href:a?c:r}:{to:i},...s,children:[o,r&&!(0,J.Z)(r)&&(0,u.jsx)(te.Z,{})]})}function pt(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(dt,{item:t})},t.href??t.to)}function ft(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(pt,{item:e},t)))})]})}function gt(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(ft,{column:e},t)))})}function ht(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function mt(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(dt,{item:t})}function bt(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(mt,{item:e}),t.length!==n+1&&(0,u.jsx)(ht,{})]},n)))})})}function vt(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(gt,{columns:t}):(0,u.jsx)(bt,{links:t})}var yt=n(28517);const _t={footerLogoLink:"footerLogoLink_BH7S"};function wt(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.C)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(yt.Z,{className:(0,o.Z)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function xt(e){let{logo:t}=e;return t.href?(0,u.jsx)(Q.Z,{href:t.href,className:_t.footerLogoLink,target:t.target,children:(0,u.jsx)(wt,{logo:t})}):(0,u.jsx)(wt,{logo:t})}function kt(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function St(e){let{style:t,links:n,logo:r,copyright:a}=e;return(0,u.jsx)("footer",{className:(0,o.Z)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||a)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),a]})]})})}function Et(){const{footer:e}=(0,_.L)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return(0,u.jsx)(St,{style:o,links:n&&n.length>0&&(0,u.jsx)(vt,{links:n}),logo:r&&(0,u.jsx)(xt,{logo:r}),copyright:t&&(0,u.jsx)(kt,{copyright:t})})}const Ct=r.memo(Et),Tt=(0,L.Qc)([D.S,w.pl,A.OC,$e.L5,s.VC,function(e){let{children:t}=e;return(0,u.jsx)(N.n2,{children:(0,u.jsx)(j.M,{children:(0,u.jsx)(R,{children:t})})})}]);function Pt(e){let{children:t}=e;return(0,u.jsx)(Tt,{children:t})}var jt=n(34055);function At(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(jt.Z,{as:"h1",className:"hero__title",children:(0,u.jsx)(c.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(et,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(tt,{error:t})})]})})})}const Lt={mainWrapper:"mainWrapper_z2l0"};function Nt(e){const{children:t,noFooter:n,wrapperClassName:r,title:i,description:c}=e;return(0,b.t)(),(0,u.jsxs)(Pt,{children:[(0,u.jsx)(s.d,{title:i,description:c}),(0,u.jsx)(y,{}),(0,u.jsx)(P,{}),(0,u.jsx)(ut,{}),(0,u.jsx)("div",{id:d,className:(0,o.Z)(m.k.wrapper.main,Lt.mainWrapper,r),children:(0,u.jsx)(a.Z,{fallback:e=>(0,u.jsx)(At,{...e}),children:t})}),!n&&(0,u.jsx)(Ct,{})]})}},49627:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(67294);var r=n(75013),o=n(51402),a=n(6832),s=n(96793),i=n(28517),c=n(85893);function l(e){let{logo:t,alt:n,imageClassName:r}=e;const a={light:(0,o.Z)(t.src),dark:(0,o.Z)(t.srcDark||t.src)},s=(0,c.jsx)(i.Z,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,c.jsx)("div",{className:r,children:s}):s}function u(e){const{siteConfig:{title:t}}=(0,a.Z)(),{navbar:{title:n,logo:i}}=(0,s.L)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,o.Z)(i?.href||"/"),g=n?"":t,h=i?.alt??g;return(0,c.jsxs)(r.Z,{to:f,...p,...i?.target&&{target:i.target},children:[i&&(0,c.jsx)(l,{logo:i,alt:h,imageClassName:u}),null!=n&&(0,c.jsx)("b",{className:d,children:n})]})}},26145:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(67294);var r=n(32411),o=n(85893);function a(e){let{locale:t,version:n,tag:a}=e;const s=t;return(0,o.jsxs)(r.Z,{children:[t&&(0,o.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,o.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,o.jsx)("meta",{name:"docusaurus_tag",content:a}),s&&(0,o.jsx)("meta",{name:"docsearch:language",content:s}),n&&(0,o.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,o.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},28517:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(67294),o=n(788),a=n(5730),s=n(70524);const i={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var c=n(85893);function l(e){let{className:t,children:n}=e;const l=(0,a.Z)(),{colorMode:u}=(0,s.I)();return(0,c.jsx)(c.Fragment,{children:(l?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,o.Z)(t,i.themedComponent,i[`themedComponent--${e}`])});return(0,c.jsx)(r.Fragment,{children:a},e)}))})}function u(e){const{sources:t,className:n,alt:r,...o}=e;return(0,c.jsx)(l,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,c.jsx)("img",{src:t[n],alt:r,className:a,...o})}})}},17940:(e,t,n)=>{"use strict";n.d(t,{u:()=>l,z:()=>b});var r=n(67294),o=n(19901),a=n(20613),s=n(39657),i=n(85893);const c="ease-in-out";function l(e){let{initialState:t}=e;const[n,o]=(0,r.useState)(t??!1),a=(0,r.useCallback)((()=>{o((e=>!e))}),[]);return{collapsed:n,setCollapsed:o,toggleCollapsed:a}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:o}=e;const a=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=o?.duration??function(e){if((0,s.n)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${o?.easing??c}`,height:`${t}px`}}function i(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(i(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{i()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,o])}function g(e){if(!o.Z.canUseDOM)return e?u:d}function h(e){let{as:t="div",collapsed:n,children:o,animation:a,onCollapseTransitionEnd:s,className:c,disableSSRStyle:l}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:a}),(0,i.jsx)(t,{ref:u,style:l?void 0:g(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),s?.(n))},className:c,children:o})}function m(e){let{collapsed:t,...n}=e;const[o,s]=(0,r.useState)(!t),[c,l]=(0,r.useState)(t);return(0,a.Z)((()=>{t||s(!0)}),[t]),(0,a.Z)((()=>{o&&l(t)}),[o,t]),o?(0,i.jsx)(h,{...n,collapsed:c}):null}function b(e){let{lazy:t,...n}=e;const r=t?m:h;return(0,i.jsx)(r,{...n})}},69061:(e,t,n)=>{"use strict";n.d(t,{nT:()=>h,pl:()=>g});var r=n(67294),o=n(5730),a=n(99200),s=n(93478),i=n(96793),c=n(85893);const l=(0,a.WA)("docusaurus.announcement.dismiss"),u=(0,a.WA)("docusaurus.announcement.id"),d=()=>"true"===l.get(),p=e=>l.set(String(e)),f=r.createContext(null);function g(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,i.L)(),t=(0,o.Z)(),[n,a]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{a(d())}),[]);const s=(0,r.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||a(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:s})),[e,n,s])}();return(0,c.jsx)(f.Provider,{value:n,children:t})}function h(){const e=(0,r.useContext)(f);if(!e)throw new s.i6("AnnouncementBarProvider");return e}},70524:(e,t,n)=>{"use strict";n.d(t,{I:()=>b,S:()=>m});var r=n(67294),o=n(19901),a=n(93478),s=n(99200),i=n(96793),c=n(85893);const l=r.createContext(void 0),u="theme",d=(0,s.WA)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,g=e=>o.Z.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),h=e=>{d.set(f(e))};function m(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,i.L)(),[o,a]=(0,r.useState)(g(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const s=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:o=!0}=r;t?(a(t),o&&h(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&s(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,s]);const c=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||c.current?c.current=window.matchMedia("print").matches:s(null)};return e.addListener(r),()=>e.removeListener(r)}),[s,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:s,get isDarkTheme(){return o===p.dark},setLightTheme(){s(p.light)},setDarkTheme(){s(p.dark)}})),[o,s])}();return(0,c.jsx)(l.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(l);if(null==e)throw new a.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},4049:(e,t,n)=>{"use strict";n.d(t,{J:()=>y,L5:()=>b,Oh:()=>_});var r=n(67294),o=n(4452),a=n(12497),s=n(96793),i=n(85919),c=n(93478),l=n(99200),u=n(85893);const d=e=>`docs-preferred-version-${e}`,p={save:(e,t,n)=>{(0,l.WA)(d(e),{persistence:t}).set(n)},read:(e,t)=>(0,l.WA)(d(e),{persistence:t}).get(),clear:(e,t)=>{(0,l.WA)(d(e),{persistence:t}).del()}},f=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const g=r.createContext(null);function h(){const e=(0,o._r)(),t=(0,s.L)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[a,i]=(0,r.useState)((()=>f(n)));(0,r.useEffect)((()=>{i(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function o(e){const t=p.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,o(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){p.save(e,t,n),i((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function m(e){let{children:t}=e;const n=h();return(0,u.jsx)(g.Provider,{value:n,children:t})}function b(e){let{children:t}=e;return i.cE?(0,u.jsx)(m,{children:t}):(0,u.jsx)(u.Fragment,{children:t})}function v(){const e=(0,r.useContext)(g);if(!e)throw new c.i6("DocsPreferredVersionContextProvider");return e}function y(e){void 0===e&&(e=a.m);const t=(0,o.zh)(e),[n,s]=v(),{preferredVersionName:i}=n[e];return{preferredVersion:t.versions.find((e=>e.name===i))??null,savePreferredVersionName:(0,r.useCallback)((t=>{s.savePreferredVersion(e,t)}),[s,e])}}function _(){const e=(0,o._r)(),[t]=v();function n(n){const r=e[n],{preferredVersionName:o}=t[n];return r.versions.find((e=>e.name===o))??null}const r=Object.keys(e);return Object.fromEntries(r.map((e=>[e,n(e)])))}},50003:(e,t,n)=>{"use strict";n.d(t,{V:()=>l,b:()=>c});var r=n(67294),o=n(93478),a=n(85893);const s=Symbol("EmptyContext"),i=r.createContext(s);function c(e){let{children:t,name:n,items:o}=e;const s=(0,r.useMemo)((()=>n&&o?{name:n,items:o}:null),[n,o]);return(0,a.jsx)(i.Provider,{value:s,children:t})}function l(){const e=(0,r.useContext)(i);if(e===s)throw new o.i6("DocsSidebarProvider");return e}},6141:(e,t,n)=>{"use strict";n.d(t,{E:()=>c,q:()=>i});var r=n(67294),o=n(93478),a=n(85893);const s=r.createContext(null);function i(e){let{children:t,version:n}=e;return(0,a.jsx)(s.Provider,{value:n,children:t})}function c(){const e=(0,r.useContext)(s);if(null===e)throw new o.i6("DocsVersionProvider");return e}},35022:(e,t,n)=>{"use strict";n.d(t,{M:()=>p,e:()=>f});var r=n(67294),o=n(82306),a=n(94980),s=n(34423),i=n(96793),c=n(93478),l=n(85893);const u=r.createContext(void 0);function d(){const e=function(){const e=(0,o.HY)(),{items:t}=(0,i.L)().navbar;return 0===t.length&&!e.component}(),t=(0,a.i)(),n=!e&&"mobile"===t,[c,l]=(0,r.useState)(!1);(0,s.Rb)((()=>{if(c)return l(!1),!1}));const u=(0,r.useCallback)((()=>{l((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&l(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:c})),[e,n,u,c])}function p(e){let{children:t}=e;const n=d();return(0,l.jsx)(u.Provider,{value:n,children:t})}function f(){const e=r.useContext(u);if(void 0===e)throw new c.i6("NavbarMobileSidebarProvider");return e}},82306:(e,t,n)=>{"use strict";n.d(t,{HY:()=>c,Zo:()=>l,n2:()=>i});var r=n(67294),o=n(93478),a=n(85893);const s=r.createContext(null);function i(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,a.jsx)(s.Provider,{value:n,children:t})}function c(){const e=(0,r.useContext)(s);if(!e)throw new o.i6("NavbarSecondaryMenuContentProvider");return e[0]}function l(e){let{component:t,props:n}=e;const a=(0,r.useContext)(s);if(!a)throw new o.i6("NavbarSecondaryMenuContentProvider");const[,i]=a,c=(0,o.Ql)(n);return(0,r.useEffect)((()=>{i({component:t,props:c})}),[i,t,c]),(0,r.useEffect)((()=>()=>i({component:null,props:null})),[i]),null}},22768:(e,t,n)=>{"use strict";n.d(t,{h:()=>o,t:()=>a});var r=n(67294);const o="navigation-with-keyboard";function a(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(o),"mousedown"===e.type&&document.body.classList.remove(o)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(o),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},9512:(e,t,n)=>{"use strict";n.d(t,{K:()=>i,M:()=>c});var r=n(67294),o=n(6832),a=n(34423);const s="q";function i(){return(0,a.Nc)(s)}function c(){const{siteConfig:{baseUrl:e,themeConfig:t}}=(0,o.Z)(),{algolia:{searchPagePath:n}}=t;return(0,r.useCallback)((t=>`${e}${n}?${s}=${encodeURIComponent(t)}`),[e,n])}},94980:(e,t,n)=>{"use strict";n.d(t,{i:()=>i});var r=n(67294),o=n(19901);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},s=996;function i(e){let{desktopBreakpoint:t=s}=void 0===e?{}:e;const[n,i]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){i(function(e){if(!o.Z.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},18015:(e,t,n)=>{"use strict";n.d(t,{k:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},39657:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{n:()=>r})},85919:(e,t,n)=>{"use strict";n.d(t,{LM:()=>f,SN:()=>S,_F:()=>m,cE:()=>p,f:()=>v,lO:()=>w,oz:()=>x,s1:()=>_,vY:()=>k});var r=n(67294),o=n(16550),a=n(18790),s=n(4452),i=n(4049),c=n(6141),l=n(50003),u=n(20636),d=n(18407);const p=!!s._r;function f(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=f(t);if(e)return e}}(e):void 0:e.href}const g=(e,t)=>void 0!==e&&(0,d.Mg)(e,t),h=(e,t)=>e.some((e=>m(e,t)));function m(e,t){return"link"===e.type?g(e.href,t):"category"===e.type&&(g(e.href,t)||h(e.items,t))}function b(e,t){switch(e.type){case"category":return m(e,t)||e.items.some((e=>b(e,t)));case"link":return!e.unlisted||m(e,t);default:return!0}}function v(e,t){return(0,r.useMemo)((()=>e.filter((e=>b(e,t)))),[e,t])}function y(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const o=[];return function e(t){for(const a of t)if("category"===a.type&&((0,d.Mg)(a.href,n)||e(a.items))||"link"===a.type&&(0,d.Mg)(a.href,n)){return r&&"category"!==a.type||o.unshift(a),!0}return!1}(t),o}function _(){const e=(0,l.V)(),{pathname:t}=(0,o.TH)(),n=(0,s.gA)()?.pluginData.breadcrumbs;return!1!==n&&e?y({sidebarItems:e.items,pathname:t}):null}function w(e){const{activeVersion:t}=(0,s.Iw)(e),{preferredVersion:n}=(0,i.J)(e),o=(0,s.yW)(e);return(0,r.useMemo)((()=>(0,u.j)([t,n,o].filter(Boolean))),[t,n,o])}function x(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function k(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,u.j)(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function S(e){let{route:t}=e;const n=(0,o.TH)(),r=(0,c.E)(),s=t.routes,i=s.find((e=>(0,o.LX)(n.pathname,e)));if(!i)return null;const l=i.sidebar,u=l?r.docsSidebars[l]:void 0;return{docElement:(0,a.H)(s),sidebarName:l,sidebarItems:u}}},71427:(e,t,n)=>{"use strict";n.d(t,{p:()=>o});var r=n(6832);function o(e){const{siteConfig:t}=(0,r.Z)(),{title:n,titleDelimiter:o}=t;return e?.trim().length?`${e.trim()} ${o} ${n}`:n}},34423:(e,t,n)=>{"use strict";n.d(t,{Nc:()=>c,Rb:()=>s,_X:()=>i});var r=n(67294),o=n(16550),a=n(93478);function s(e){!function(e){const t=(0,o.k6)(),n=(0,a.zX)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}function i(e){return function(e){const t=(0,o.k6)();return(0,r.useSyncExternalStore)(t.listen,(()=>e(t)),(()=>e(t)))}((t=>null===e?null:new URLSearchParams(t.location.search).get(e)))}function c(e){const t=i(e)??"",n=function(){const e=(0,o.k6)();return(0,r.useCallback)(((t,n,r)=>{const o=new URLSearchParams(e.location.search);n?o.set(t,n):o.delete(t),(r?.push?e.push:e.replace)({search:o.toString()})}),[e])}();return[t,(0,r.useCallback)(((t,r)=>{n(e,t,r)}),[n,e])]}},20636:(e,t,n)=>{"use strict";function r(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,r)=>e.findIndex((e=>t(e,n)))!==r))}function o(e){return Array.from(new Set(e))}n.d(t,{j:()=>o,l:()=>r})},44873:(e,t,n)=>{"use strict";n.d(t,{FG:()=>f,d:()=>d,VC:()=>g});var r=n(67294),o=n(788),a=n(32411),s=n(66041);function i(){const e=r.useContext(s._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var c=n(51402),l=n(71427),u=n(85893);function d(e){let{title:t,description:n,keywords:r,image:o,children:s}=e;const i=(0,l.p)(t),{withBaseUrl:d}=(0,c.C)(),p=o?d(o,{absolute:!0}):void 0;return(0,u.jsxs)(a.Z,{children:[t&&(0,u.jsx)("title",{children:i}),t&&(0,u.jsx)("meta",{property:"og:title",content:i}),n&&(0,u.jsx)("meta",{name:"description",content:n}),n&&(0,u.jsx)("meta",{property:"og:description",content:n}),r&&(0,u.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,u.jsx)("meta",{property:"og:image",content:p}),p&&(0,u.jsx)("meta",{name:"twitter:image",content:p}),s]})}const p=r.createContext(void 0);function f(e){let{className:t,children:n}=e;const s=r.useContext(p),i=(0,o.Z)(s,t);return(0,u.jsxs)(p.Provider,{value:i,children:[(0,u.jsx)(a.Z,{children:(0,u.jsx)("html",{className:i})}),n]})}function g(e){let{children:t}=e;const n=i(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,u.jsx)(f,{className:(0,o.Z)(r,a),children:t})}},93478:(e,t,n)=>{"use strict";n.d(t,{D9:()=>i,Qc:()=>u,Ql:()=>l,i6:()=>c,zX:()=>s});var r=n(67294),o=n(20613),a=n(85893);function s(e){const t=(0,r.useRef)(e);return(0,o.Z)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function i(e){const t=(0,r.useRef)();return(0,o.Z)((()=>{t.current=e})),t.current}class c extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(? \w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function l(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},88648:(e,t,n)=>{"use strict";function r(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}n.d(t,{F:()=>r})},18407:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>s,Ns:()=>i});var r=n(67294),o=n(21204),a=n(6832);function s(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function i(){const{baseUrl:e}=(0,a.Z)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function o(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(o).flatMap((e=>e.routes??[])))}(n)}({routes:o.Z,baseUrl:e})),[e])}},63735:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>h,OC:()=>u,RF:()=>f,o5:()=>g});var r=n(67294),o=n(19901),a=n(5730),s=n(20613),i=n(93478),c=n(85893);const l=r.createContext(void 0);function u(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,c.jsx)(l.Provider,{value:n,children:t})}function d(){const e=(0,r.useContext)(l);if(null==e)throw new i.i6("ScrollControllerProvider");return e}const p=()=>o.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function f(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=d(),o=(0,r.useRef)(p()),a=(0,i.zX)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=p();a(e,o.current),o.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function g(){const e=d(),t=function(){const e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,r.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}}),[]);return(0,r.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,r.useRef)(void 0),o=(0,r.useCallback)((r=>{t.save(r),e.disableScrollEvents(),n.current=()=>{const{restored:r}=t.restore();if(n.current=void 0,r){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,s.Z)((()=>{queueMicrotask((()=>n.current?.()))})),{blockElementScrollPositionUntilNextRender:o}}function h(){const e=(0,r.useRef)(null),t=(0,a.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const o=document.documentElement.scrollTop;(n&&o>e||!n&&o t&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},39105:(e,t,n)=>{"use strict";n.d(t,{HX:()=>s,_q:()=>c,os:()=>i});var r=n(4452),o=n(6832),a=n(4049);const s="default";function i(e,t){return`docs-${e}-${t}`}function c(){const{i18n:e}=(0,o.Z)(),t=(0,r._r)(),n=(0,r.WS)(),c=(0,a.Oh)();const l=[s,...Object.keys(t).map((function(e){const r=n?.activePlugin.pluginId===e?n.activeVersion:void 0,o=c[e],a=t[e].versions.find((e=>e.isLast));return i(e,(r??o??a).name)}))];return{locale:e.currentLocale,tags:l}}},99200:(e,t,n)=>{"use strict";n.d(t,{Nk:()=>u,WA:()=>l});var r=n(67294);const o="localStorage";function a(e){let{key:t,oldValue:n,newValue:r,storage:o}=e;if(n===r)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,o),window.dispatchEvent(a)}function s(e){if(void 0===e&&(e=o),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,i||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),i=!0),null}var t}let i=!1;const c={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function l(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(e);const n=s(t?.persistence);return null===n?c:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{const r=n.getItem(e);n.setItem(e,t),a({key:e,oldValue:r,newValue:t,storage:n})}catch(r){console.error(`Docusaurus storage error, can't set ${e}=${t}`,r)}},del:()=>{try{const t=n.getItem(e);n.removeItem(e),a({key:e,oldValue:t,newValue:null,storage:n})}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}},listen:t=>{try{const r=r=>{r.storageArea===n&&r.key===e&&t(r)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}catch(r){return console.error(`Docusaurus storage error, can't listen for changes of key=${e}`,r),()=>{}}}}}function u(e,t){const n=(0,r.useRef)((()=>null===e?c:l(e,t))).current(),o=(0,r.useCallback)((e=>"undefined"==typeof window?()=>{}:n.listen(e)),[n]);return[(0,r.useSyncExternalStore)(o,(()=>"undefined"==typeof window?null:n.get()),(()=>null)),n]}},13156:(e,t,n)=>{"use strict";n.d(t,{l:()=>s});var r=n(6832),o=n(16550),a=n(79861);function s(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:s,currentLocale:i}}=(0,r.Z)(),{pathname:c}=(0,o.TH)(),l=(0,a.applyTrailingSlash)(c,{trailingSlash:n,baseUrl:e}),u=i===s?e:e.replace(`/${i}/`,"/"),d=l.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===s?`${u}`:`${u}${e}/`}(n)}${d}`}}}},68265:(e,t,n)=>{"use strict";n.d(t,{S:()=>s});var r=n(67294),o=n(16550),a=n(93478);function s(e){const t=(0,o.TH)(),n=(0,a.D9)(t),s=(0,a.zX)(e);(0,r.useEffect)((()=>{n&&t!==n&&s({location:t,previousLocation:n})}),[s,t,n])}},96793:(e,t,n)=>{"use strict";n.d(t,{L:()=>o});var r=n(6832);function o(){return(0,r.Z)().siteConfig.themeConfig}},12057:(e,t,n)=>{"use strict";n.d(t,{L:()=>o});var r=n(6832);function o(){const{siteConfig:{themeConfig:e}}=(0,r.Z)();return e}},80180:(e,t,n)=>{"use strict";n.d(t,{l:()=>i});var r=n(67294),o=n(88648),a=n(51402),s=n(12057);function i(){const{withBaseUrl:e}=(0,a.C)(),{algolia:{externalUrlRegex:t,replaceSearchResultPathname:n}}=(0,s.L)();return(0,r.useCallback)((r=>{const a=new URL(r);if((0,o.F)(t,a.href))return r;const s=`${a.pathname+a.hash}`;return e(function(e,t){return t?e.replaceAll(new RegExp(t.from,"g"),t.to):e}(s,n))}),[e,t,n])}},54357:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[o]=e.split(/[#?]/),a="/"===o||o===r?o:(s=o,n?function(e){return e.endsWith("/")?e:`${e}/`}(s):function(e){return e.endsWith("/")?e.slice(0,-1):e}(s));var s;return e.replace(o,a)}},6009:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=void 0,t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},79861:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="__blog-post-container";var o=n(54357);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r(o).default}});var a=n(6009);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return a.getErrorCausalChain}})},99318:(e,t,n)=>{"use strict";n.d(t,{lX:()=>_,q_:()=>C,ob:()=>f,PP:()=>P,Ep:()=>p});var r=n(87462);function o(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,r=n+1,o=e.length;r =0;p--){var f=s[p];"."===f?a(s,p):".."===f?(a(s,p),d++):d&&(a(s,p),d--)}if(!l)for(;d--;d)s.unshift("..");!l||""===s[0]||s[0]&&o(s[0])||s.unshift("");var g=s.join("/");return n&&"/"!==g.substr(-1)&&(g+="/"),g};var i=n(38776);function c(e){return"/"===e.charAt(0)?e:"/"+e}function l(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),a.state=t):(void 0===(a=(0,r.Z)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(i){throw i instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):i}return n&&(a.key=n),o?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=s(a.pathname,o.pathname)):a.pathname=o.pathname:a.pathname||(a.pathname="/"),a}function g(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof r?r(a,o):o(!0):o(!1!==a)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;r t?n.splice(t,n.length-t,o):n.push(o),d({action:r,location:o,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",o=f(e,t,h(),_.location);u.confirmTransitionTo(o,r,n,(function(e){e&&(_.entries[_.index]=o,d({action:r,location:o}))}))},go:y,goBack:function(){y(-1)},goForward:function(){y(1)},canGo:function(e){var t=_.index+e;return t>=0&&t<_.entries.length},block:function(e){return void 0===e&&(e=!1),u.setPrompt(e)},listen:function(e){return u.appendListener(e)}};return _}},8679:(e,t,n)=>{"use strict";var r=n(59864),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},s={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function c(e){return r.isMemo(e)?s:i[e.$$typeof]||o}i[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[r.Memo]=s;var l=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,g=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(g){var o=f(n);o&&o!==g&&e(t,o,r)}var s=u(n);d&&(s=s.concat(d(n)));for(var i=c(t),h=c(n),m=0;m {"use strict";e.exports=function(e,t,n,r,o,a,s,i){if(!e){var c;if(void 0===t)c=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var l=[n,r,o,a,s,i],u=0;(c=new Error(t.replace(/%s/g,(function(){return l[u++]})))).name="Invariant Violation"}throw c.framesToPop=1,c}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},32497:(e,t,n)=>{"use strict";n.r(t)},52295:(e,t,n)=>{"use strict";n.r(t)},74865:function(e,t,n){var r,o;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:' '};function o(e,t,n){return en?n:e}function a(e){return 100*(-1+e)}function s(e,t,n){var o;return(o="translate3d"===r.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,o}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=o(e,r.minimum,1),n.status=1===e?null:e;var a=n.render(!t),l=a.querySelector(r.barSelector),u=r.speed,d=r.easing;return a.offsetWidth,i((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),c(l,s(e,u,d)),1===e?(c(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){c(a,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var o,s=t.querySelector(r.barSelector),i=e?"-100":a(n.status||0),l=document.querySelector(r.parent);return c(s,{transition:"all 0 linear",transform:"translate3d("+i+"%,0,0)"}),r.showSpinner||(o=t.querySelector(r.spinnerSelector))&&f(o),l!=document.body&&u(l,"nprogress-custom-parent"),l.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var i=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),c=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,o=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);o--;)if((r=e[o]+a)in n)return r;return t}function o(e){return e=n(e),t[e]||(t[e]=r(e))}function a(e,t,n){t=o(t),e.style[t]=n}return function(e,t){var n,r,o=arguments;if(2==o.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&a(e,n,r);else a(e,o[1],o[2])}}();function l(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;l(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);l(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},57065:()=>{!function(e){var t=[/\b(?:async|sync|yield)\*/,/\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/],n=/(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}}}};e.languages.dart=e.languages.extend("clike",{"class-name":[r,{pattern:RegExp(n+/[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source),lookbehind:!0,inside:r.inside}],keyword:t,operator:/\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*\/%&^|=!<>]=?|\?/}),e.languages.insertBefore("dart","string",{"string-literal":{pattern:/r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,lookbehind:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:e.languages.dart}}},string:/[\s\S]+/}},string:void 0}),e.languages.insertBefore("dart","class-name",{metadata:{pattern:/@\w+/,alias:"function"}}),e.languages.insertBefore("dart","class-name",{generics:{pattern:/<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism)},52503:()=>{!function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"/,lookbehind:!0,greedy:!0},"class-name":[r,{pattern:RegExp(/(^|[^\w.])/.source+n+/[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/.source),lookbehind:!0,inside:r.inside},{pattern:RegExp(/(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/.source+n+/[A-Z]\w*\b/.source),lookbehind:!0,inside:r.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\b[A-Z][A-Z_\d]+\b/}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"},char:{pattern:/'(?:\\.|[^'\\\r\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(/(\bimport\s+)/.source+n+/(?:[A-Z]\w*|\*)(?=\s*;)/.source),lookbehind:!0,inside:{namespace:r.inside.namespace,punctuation:/\./,operator:/\*/,"class-name":/\w+/}},{pattern:RegExp(/(\bimport\s+static\s+)/.source+n+/(?:\w+|\*)(?=\s*;)/.source),lookbehind:!0,alias:"static",inside:{namespace:r.inside.namespace,static:/\b\w+$/,punctuation:/\./,operator:/\*/,"class-name":/\w+/}}],namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?! )[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(/ /g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}})}(Prism)},96854:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,o,a){if(n.language===r){var s=n.tokenStack=[];n.code=n.code.replace(o,(function(e){if("function"==typeof a&&!a(e))return e;for(var o,i=s.length;-1!==n.code.indexOf(o=t(r,i));)++i;return s[i]=e,o})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var o=0,a=Object.keys(n.tokenStack);!function s(i){for(var c=0;c =a.length);c++){var l=i[c];if("string"==typeof l||l.content&&"string"==typeof l.content){var u=a[o],d=n.tokenStack[u],p="string"==typeof l?l:l.content,f=t(r,u),g=p.indexOf(f);if(g>-1){++o;var h=p.substring(0,g),m=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(g+f.length),v=[];h&&v.push.apply(v,s([h])),v.push(m),b&&v.push.apply(v,s([b])),"string"==typeof l?i.splice.apply(i,[c,1].concat(v)):l.content=v}}else l.content&&s(l.content)}return i}(n.tokens)}}}})}(Prism)},99945:()=>{!function(e){var t=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,n=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],r=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,o=/=>|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,a=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:t,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:r,operator:o,punctuation:a};var s={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},i=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:s}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:s}}];e.languages.insertBefore("php","variable",{string:i,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:t,string:i,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,number:r,operator:o,punctuation:a}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(t){if(/<\?/.test(t.code)){e.languages["markup-templating"].buildPlaceholders(t,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g)}})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"php")}))}(Prism)},90874:()=>{Prism.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp(/(^|[^"#])/.source+"(?:"+/"(?:\\(?:\((?:[^()]|\([^()]*\))*\)|\r\n|[^(])|[^\\\r\n"])*"/.source+"|"+/"""(?:\\(?:\((?:[^()]|\([^()]*\))*\)|[^(])|[^\\"]|"(?!""))*"""/.source+")"+/(?!["#])/.source),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp(/(^|[^"#])(#+)/.source+"(?:"+/"(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|\r\n|[^#])|[^\\\r\n])*?"/.source+"|"+/"""(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|[^#])|[^\\])*?"""/.source+")\\2"),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp(/#/.source+"(?:"+/(?:elseif|if)\b/.source+"(?:[ \t]*"+/(?:![ \t]*)?(?:\b\w+\b(?:[ \t]*\((?:[^()]|\([^()]*\))*\))?|\((?:[^()]|\([^()]*\))*\))(?:[ \t]*(?:&&|\|\|))?/.source+")+|"+/(?:else|endif)\b/.source+")"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:false|true)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},Prism.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift}))},30977:(e,t,n)=>{var r={"./prism-dart":57065,"./prism-java":52503,"./prism-php":99945,"./prism-swift":90874};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=30977},92703:(e,t,n)=>{"use strict";var r=n(50414);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,s){if(s!==r){var i=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw i.name="Invariant Violation",i}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},45697:(e,t,n)=>{e.exports=n(92703)()},50414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},64448:(e,t,n)=>{"use strict";var r=n(67294),o=n(63840);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n t}return!1}(t,n,o,r)&&(n=null),r||null===o?function(e){return!!d.call(g,e)||!d.call(f,e)&&(p.test(e)?g[e]=!0:(f[e]=!0,!1))}(t)&&(null===n?e.removeAttribute(t):e.setAttribute(t,""+n)):o.mustUseProperty?e[o.propertyName]=null===n?3!==o.type&&"":n:(t=o.attributeName,r=o.attributeNamespace,null===n?e.removeAttribute(t):(n=3===(o=o.type)||4===o&&!0===n?"":""+n,r?e.setAttributeNS(r,t,n):e.setAttribute(t,n))))}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach((function(e){var t=e.replace(b,v);m[t]=new h(t,1,!1,e,null,!1,!1)})),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach((function(e){var t=e.replace(b,v);m[t]=new h(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)})),["xml:base","xml:lang","xml:space"].forEach((function(e){var t=e.replace(b,v);m[t]=new h(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)})),["tabIndex","crossOrigin"].forEach((function(e){m[e]=new h(e,1,!1,e.toLowerCase(),null,!1,!1)})),m.xlinkHref=new h("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach((function(e){m[e]=new h(e,1,!1,e.toLowerCase(),null,!0,!0)}));var _=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,w=Symbol.for("react.element"),x=Symbol.for("react.portal"),k=Symbol.for("react.fragment"),S=Symbol.for("react.strict_mode"),E=Symbol.for("react.profiler"),C=Symbol.for("react.provider"),T=Symbol.for("react.context"),P=Symbol.for("react.forward_ref"),j=Symbol.for("react.suspense"),A=Symbol.for("react.suspense_list"),L=Symbol.for("react.memo"),N=Symbol.for("react.lazy");Symbol.for("react.scope"),Symbol.for("react.debug_trace_mode");var I=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden"),Symbol.for("react.cache"),Symbol.for("react.tracing_marker");var R=Symbol.iterator;function O(e){return null===e||"object"!=typeof e?null:"function"==typeof(e=R&&e[R]||e["@@iterator"])?e:null}var M,F=Object.assign;function D(e){if(void 0===M)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);M=t&&t[1]||""}return"\n"+M+e}var z=!1;function B(e,t){if(!e||z)return"";z=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),"object"==typeof Reflect&&Reflect.construct){try{Reflect.construct(t,[])}catch(l){var r=l}Reflect.construct(e,[],t)}else{try{t.call()}catch(l){r=l}e.call(t.prototype)}else{try{throw Error()}catch(l){r=l}e()}}catch(l){if(l&&r&&"string"==typeof l.stack){for(var o=l.stack.split("\n"),a=r.stack.split("\n"),s=o.length-1,i=a.length-1;1<=s&&0<=i&&o[s]!==a[i];)i--;for(;1<=s&&0<=i;s--,i--)if(o[s]!==a[i]){if(1!==s||1!==i)do{if(s--,0>--i||o[s]!==a[i]){var c="\n"+o[s].replace(" at new "," at ");return e.displayName&&c.includes(" ")&&(c=c.replace(" ",e.displayName)),c}}while(1<=s&&0<=i);break}}}finally{z=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?D(e):""}function $(e){switch(e.tag){case 5:return D(e.type);case 16:return D("Lazy");case 13:return D("Suspense");case 19:return D("SuspenseList");case 0:case 2:case 15:return e=B(e.type,!1);case 11:return e=B(e.type.render,!1);case 1:return e=B(e.type,!0);default:return""}}function U(e){if(null==e)return null;if("function"==typeof e)return e.displayName||e.name||null;if("string"==typeof e)return e;switch(e){case k:return"Fragment";case x:return"Portal";case E:return"Profiler";case S:return"StrictMode";case j:return"Suspense";case A:return"SuspenseList"}if("object"==typeof e)switch(e.$$typeof){case T:return(e.displayName||"Context")+".Consumer";case C:return(e._context.displayName||"Context")+".Provider";case P:var t=e.render;return(e=e.displayName)||(e=""!==(e=t.displayName||t.name||"")?"ForwardRef("+e+")":"ForwardRef"),e;case L:return null!==(t=e.displayName||null)?t:U(e.type)||"Memo";case N:t=e._payload,e=e._init;try{return U(e(t))}catch(n){}}return null}function Z(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=(e=t.render).displayName||e.name||"",t.displayName||(""!==e?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return U(t);case 8:return t===S?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"==typeof t)return t.displayName||t.name||null;if("string"==typeof t)return t}return null}function H(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":case"object":return e;default:return""}}function G(e){var t=e.type;return(e=e.nodeName)&&"input"===e.toLowerCase()&&("checkbox"===t||"radio"===t)}function q(e){e._valueTracker||(e._valueTracker=function(e){var t=G(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&void 0!==n&&"function"==typeof n.get&&"function"==typeof n.set){var o=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(e){r=""+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=""+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}(e))}function V(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=G(e)?e.checked?"true":"false":e.value),(e=r)!==n&&(t.setValue(e),!0)}function W(e){if(void 0===(e=e||("undefined"!=typeof document?document:void 0)))return null;try{return e.activeElement||e.body}catch(t){return e.body}}function K(e,t){var n=t.checked;return F({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=n?n:e._wrapperState.initialChecked})}function Y(e,t){var n=null==t.defaultValue?"":t.defaultValue,r=null!=t.checked?t.checked:t.defaultChecked;n=H(null!=t.value?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:"checkbox"===t.type||"radio"===t.type?null!=t.checked:null!=t.value}}function Q(e,t){null!=(t=t.checked)&&y(e,"checked",t,!1)}function X(e,t){Q(e,t);var n=H(t.value),r=t.type;if(null!=n)"number"===r?(0===n&&""===e.value||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if("submit"===r||"reset"===r)return void e.removeAttribute("value");t.hasOwnProperty("value")?ee(e,t.type,n):t.hasOwnProperty("defaultValue")&&ee(e,t.type,H(t.defaultValue)),null==t.checked&&null!=t.defaultChecked&&(e.defaultChecked=!!t.defaultChecked)}function J(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!("submit"!==r&&"reset"!==r||void 0!==t.value&&null!==t.value))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}""!==(n=e.name)&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,""!==n&&(e.name=n)}function ee(e,t,n){"number"===t&&W(e.ownerDocument)===e||(null==n?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var te=Array.isArray;function ne(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o "+t.valueOf().toString()+"",t=le.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}},"undefined"!=typeof MSApp&&MSApp.execUnsafeLocalFunction?function(e,t,n,r){MSApp.execUnsafeLocalFunction((function(){return ue(e,t)}))}:ue);function pe(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&3===n.nodeType)return void(n.nodeValue=t)}e.textContent=t}var fe={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},ge=["Webkit","ms","Moz","O"];function he(e,t,n){return null==t||"boolean"==typeof t||""===t?"":n||"number"!=typeof t||0===t||fe.hasOwnProperty(e)&&fe[e]?(""+t).trim():t+"px"}function me(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=0===n.indexOf("--"),o=he(n,t[n],r);"float"===n&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}Object.keys(fe).forEach((function(e){ge.forEach((function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),fe[t]=fe[e]}))}));var be=F({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ve(e,t){if(t){if(be[e]&&(null!=t.children||null!=t.dangerouslySetInnerHTML))throw Error(a(137,e));if(null!=t.dangerouslySetInnerHTML){if(null!=t.children)throw Error(a(60));if("object"!=typeof t.dangerouslySetInnerHTML||!("__html"in t.dangerouslySetInnerHTML))throw Error(a(61))}if(null!=t.style&&"object"!=typeof t.style)throw Error(a(62))}}function ye(e,t){if(-1===e.indexOf("-"))return"string"==typeof t.is;switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var _e=null;function we(e){return(e=e.target||e.srcElement||window).correspondingUseElement&&(e=e.correspondingUseElement),3===e.nodeType?e.parentNode:e}var xe=null,ke=null,Se=null;function Ee(e){if(e=_o(e)){if("function"!=typeof xe)throw Error(a(280));var t=e.stateNode;t&&(t=xo(t),xe(e.stateNode,e.type,t))}}function Ce(e){ke?Se?Se.push(e):Se=[e]:ke=e}function Te(){if(ke){var e=ke,t=Se;if(Se=ke=null,Ee(e),t)for(e=0;e >>=0,0===e?32:31-(it(e)/ct|0)|0},it=Math.log,ct=Math.LN2;var lt=64,ut=4194304;function dt(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194240&e;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return 130023424&e;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function pt(e,t){var n=e.pendingLanes;if(0===n)return 0;var r=0,o=e.suspendedLanes,a=e.pingedLanes,s=268435455&n;if(0!==s){var i=s&~o;0!==i?r=dt(i):0!==(a&=s)&&(r=dt(a))}else 0!==(s=n&~o)?r=dt(s):0!==a&&(r=dt(a));if(0===r)return 0;if(0!==t&&t!==r&&0==(t&o)&&((o=r&-r)>=(a=t&-t)||16===o&&0!=(4194240&a)))return t;if(0!=(4&r)&&(r|=16&n),0!==(t=e.entangledLanes))for(e=e.entanglements,t&=r;0 n;n++)t.push(e);return t}function bt(e,t,n){e.pendingLanes|=t,536870912!==t&&(e.suspendedLanes=0,e.pingedLanes=0),(e=e.eventTimes)[t=31-st(t)]=n}function vt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-st(n),o=1< =On),Dn=String.fromCharCode(32),zn=!1;function Bn(e,t){switch(e){case"keyup":return-1!==In.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function $n(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Un=!1;var Zn={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Hn(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===t?!!Zn[e.type]:"textarea"===t}function Gn(e,t,n,r){Ce(r),0<(t=qr(t,"onChange")).length&&(n=new un("onChange","change",null,n,r),e.push({event:n,listeners:t}))}var qn=null,Vn=null;function Wn(e){Dr(e,0)}function Kn(e){if(V(wo(e)))return e}function Yn(e,t){if("change"===e)return t}var Qn=!1;if(u){var Xn;if(u){var Jn="oninput"in document;if(!Jn){var er=document.createElement("div");er.setAttribute("oninput","return;"),Jn="function"==typeof er.oninput}Xn=Jn}else Xn=!1;Qn=Xn&&(!document.documentMode||9 =t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=lr(r)}}function dr(e,t){return!(!e||!t)&&(e===t||(!e||3!==e.nodeType)&&(t&&3===t.nodeType?dr(e,t.parentNode):"contains"in e?e.contains(t):!!e.compareDocumentPosition&&!!(16&e.compareDocumentPosition(t))))}function pr(){for(var e=window,t=W();t instanceof e.HTMLIFrameElement;){try{var n="string"==typeof t.contentWindow.location.href}catch(r){n=!1}if(!n)break;t=W((e=t.contentWindow).document)}return t}function fr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&("input"===t&&("text"===e.type||"search"===e.type||"tel"===e.type||"url"===e.type||"password"===e.type)||"textarea"===t||"true"===e.contentEditable)}function gr(e){var t=pr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&dr(n.ownerDocument.documentElement,n)){if(null!==r&&fr(n))if(t=r.start,void 0===(e=r.end)&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if((e=(t=n.ownerDocument||document)&&t.defaultView||window).getSelection){e=e.getSelection();var o=n.textContent.length,a=Math.min(r.start,o);r=void 0===r.end?a:Math.min(r.end,o),!e.extend&&a>r&&(o=r,r=a,a=o),o=ur(n,a);var s=ur(n,r);o&&s&&(1!==e.rangeCount||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&((t=t.createRange()).setStart(o.node,o.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}for(t=[],e=n;e=e.parentNode;)1===e.nodeType&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for("function"==typeof n.focus&&n.focus(),n=0;n =document.documentMode,mr=null,br=null,vr=null,yr=!1;function _r(e,t,n){var r=n.window===n?n.document:9===n.nodeType?n:n.ownerDocument;yr||null==mr||mr!==W(r)||("selectionStart"in(r=mr)&&fr(r)?r={start:r.selectionStart,end:r.selectionEnd}:r={anchorNode:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset},vr&&cr(vr,r)||(vr=r,0<(r=qr(br,"onSelect")).length&&(t=new un("onSelect","select",null,t,n),e.push({event:t,listeners:r}),t.target=mr)))}function wr(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n["Webkit"+e]="webkit"+t,n["Moz"+e]="moz"+t,n}var xr={animationend:wr("Animation","AnimationEnd"),animationiteration:wr("Animation","AnimationIteration"),animationstart:wr("Animation","AnimationStart"),transitionend:wr("Transition","TransitionEnd")},kr={},Sr={};function Er(e){if(kr[e])return kr[e];if(!xr[e])return e;var t,n=xr[e];for(t in n)if(n.hasOwnProperty(t)&&t in Sr)return kr[e]=n[t];return e}u&&(Sr=document.createElement("div").style,"AnimationEvent"in window||(delete xr.animationend.animation,delete xr.animationiteration.animation,delete xr.animationstart.animation),"TransitionEvent"in window||delete xr.transitionend.transition);var Cr=Er("animationend"),Tr=Er("animationiteration"),Pr=Er("animationstart"),jr=Er("transitionend"),Ar=new Map,Lr="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function Nr(e,t){Ar.set(e,t),c(t,[e])}for(var Ir=0;Ir So||(e.current=ko[So],ko[So]=null,So--)}function To(e,t){So++,ko[So]=e.current,e.current=t}var Po={},jo=Eo(Po),Ao=Eo(!1),Lo=Po;function No(e,t){var n=e.type.contextTypes;if(!n)return Po;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o,a={};for(o in n)a[o]=t[o];return r&&((e=e.stateNode).__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function Io(e){return null!=(e=e.childContextTypes)}function Ro(){Co(Ao),Co(jo)}function Oo(e,t,n){if(jo.current!==Po)throw Error(a(168));To(jo,t),To(Ao,n)}function Mo(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,"function"!=typeof r.getChildContext)return n;for(var o in r=r.getChildContext())if(!(o in t))throw Error(a(108,Z(e)||"Unknown",o));return F({},n,r)}function Fo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Po,Lo=jo.current,To(jo,e),To(Ao,Ao.current),!0}function Do(e,t,n){var r=e.stateNode;if(!r)throw Error(a(169));n?(e=Mo(e,t,Lo),r.__reactInternalMemoizedMergedChildContext=e,Co(Ao),Co(jo),To(jo,e)):Co(Ao),To(Ao,n)}var zo=null,Bo=!1,$o=!1;function Uo(e){null===zo?zo=[e]:zo.push(e)}function Zo(){if(!$o&&null!==zo){$o=!0;var e=0,t=yt;try{var n=zo;for(yt=1;e >=s,o-=s,Qo=1<<32-st(t)+o|n< h?(m=d,d=null):m=d.sibling;var b=f(o,d,i[h],c);if(null===b){null===d&&(d=m);break}e&&d&&null===b.alternate&&t(o,d),a=s(b,a,h),null===u?l=b:u.sibling=b,u=b,d=m}if(h===i.length)return n(o,d),aa&&Jo(o,h),l;if(null===d){for(;h m?(b=h,h=null):b=h.sibling;var y=f(o,h,v.value,l);if(null===y){null===h&&(h=b);break}e&&h&&null===y.alternate&&t(o,h),i=s(y,i,m),null===d?u=y:d.sibling=y,d=y,h=b}if(v.done)return n(o,h),aa&&Jo(o,m),u;if(null===h){for(;!v.done;m++,v=c.next())null!==(v=p(o,v.value,l))&&(i=s(v,i,m),null===d?u=v:d.sibling=v,d=v);return aa&&Jo(o,m),u}for(h=r(o,h);!v.done;m++,v=c.next())null!==(v=g(h,o,m,v.value,l))&&(e&&null!==v.alternate&&h.delete(null===v.key?m:v.key),i=s(v,i,m),null===d?u=v:d.sibling=v,d=v);return e&&h.forEach((function(e){return t(o,e)})),aa&&Jo(o,m),u}return function e(r,a,s,c){if("object"==typeof s&&null!==s&&s.type===k&&null===s.key&&(s=s.props.children),"object"==typeof s&&null!==s){switch(s.$$typeof){case w:e:{for(var l=s.key,u=a;null!==u;){if(u.key===l){if((l=s.type)===k){if(7===u.tag){n(r,u.sibling),(a=o(u,s.props.children)).return=r,r=a;break e}}else if(u.elementType===l||"object"==typeof l&&null!==l&&l.$$typeof===N&&Ka(l)===u.type){n(r,u.sibling),(a=o(u,s.props)).ref=Va(r,u,s),a.return=r,r=a;break e}n(r,u);break}t(r,u),u=u.sibling}s.type===k?((a=Ml(s.props.children,r.mode,c,s.key)).return=r,r=a):((c=Ol(s.type,s.key,s.props,null,r.mode,c)).ref=Va(r,a,s),c.return=r,r=c)}return i(r);case x:e:{for(u=s.key;null!==a;){if(a.key===u){if(4===a.tag&&a.stateNode.containerInfo===s.containerInfo&&a.stateNode.implementation===s.implementation){n(r,a.sibling),(a=o(a,s.children||[])).return=r,r=a;break e}n(r,a);break}t(r,a),a=a.sibling}(a=zl(s,r.mode,c)).return=r,r=a}return i(r);case N:return e(r,a,(u=s._init)(s._payload),c)}if(te(s))return h(r,a,s,c);if(O(s))return m(r,a,s,c);Wa(r,s)}return"string"==typeof s&&""!==s||"number"==typeof s?(s=""+s,null!==a&&6===a.tag?(n(r,a.sibling),(a=o(a,s)).return=r,r=a):(n(r,a),(a=Dl(s,r.mode,c)).return=r,r=a),i(r)):n(r,a)}}var Qa=Ya(!0),Xa=Ya(!1),Ja={},es=Eo(Ja),ts=Eo(Ja),ns=Eo(Ja);function rs(e){if(e===Ja)throw Error(a(174));return e}function os(e,t){switch(To(ns,t),To(ts,e),To(es,Ja),e=t.nodeType){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ce(null,"");break;default:t=ce(t=(e=8===e?t.parentNode:t).namespaceURI||null,e=e.tagName)}Co(es),To(es,t)}function as(){Co(es),Co(ts),Co(ns)}function ss(e){rs(ns.current);var t=rs(es.current),n=ce(t,e.type);t!==n&&(To(ts,e),To(es,n))}function is(e){ts.current===e&&(Co(es),Co(ts))}var cs=Eo(0);function ls(e){for(var t=e;null!==t;){if(13===t.tag){var n=t.memoizedState;if(null!==n&&(null===(n=n.dehydrated)||"$?"===n.data||"$!"===n.data))return t}else if(19===t.tag&&void 0!==t.memoizedProps.revealOrder){if(0!=(128&t.flags))return t}else if(null!==t.child){t.child.return=t,t=t.child;continue}if(t===e)break;for(;null===t.sibling;){if(null===t.return||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var us=[];function ds(){for(var e=0;e n?n:4,e(!0);var r=fs.transition;fs.transition={};try{e(!1),t()}finally{yt=n,fs.transition=r}}function ei(){return Ts().memoizedState}function ti(e,t,n){var r=nl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ri(e))oi(t,n);else if(null!==(n=ja(e,t,n,r))){rl(n,e,r,tl()),ai(n,t,r)}}function ni(e,t,n){var r=nl(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ri(e))oi(t,o);else{var a=e.alternate;if(0===e.lanes&&(null===a||0===a.lanes)&&null!==(a=t.lastRenderedReducer))try{var s=t.lastRenderedState,i=a(s,n);if(o.hasEagerState=!0,o.eagerState=i,ir(i,s)){var c=t.interleaved;return null===c?(o.next=o,Pa(t)):(o.next=c.next,c.next=o),void(t.interleaved=o)}}catch(l){}null!==(n=ja(e,t,o,r))&&(rl(n,e,r,o=tl()),ai(n,t,r))}}function ri(e){var t=e.alternate;return e===hs||null!==t&&t===hs}function oi(e,t){ys=vs=!0;var n=e.pending;null===n?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ai(e,t,n){if(0!=(4194240&n)){var r=t.lanes;n|=r&=e.pendingLanes,t.lanes=n,vt(e,n)}}var si={readContext:Ca,useCallback:xs,useContext:xs,useEffect:xs,useImperativeHandle:xs,useInsertionEffect:xs,useLayoutEffect:xs,useMemo:xs,useReducer:xs,useRef:xs,useState:xs,useDebugValue:xs,useDeferredValue:xs,useTransition:xs,useMutableSource:xs,useSyncExternalStore:xs,useId:xs,unstable_isNewReconciler:!1},ii={readContext:Ca,useCallback:function(e,t){return Cs().memoizedState=[e,void 0===t?null:t],e},useContext:Ca,useEffect:Zs,useImperativeHandle:function(e,t,n){return n=null!=n?n.concat([e]):null,$s(4194308,4,Vs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $s(4194308,4,e,t)},useInsertionEffect:function(e,t){return $s(4,2,e,t)},useMemo:function(e,t){var n=Cs();return t=void 0===t?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Cs();return t=void 0!==n?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=ti.bind(null,hs,e),[r.memoizedState,e]},useRef:function(e){return e={current:e},Cs().memoizedState=e},useState:Ds,useDebugValue:Ks,useDeferredValue:function(e){return Cs().memoizedState=e},useTransition:function(){var e=Ds(!1),t=e[0];return e=Js.bind(null,e[1]),Cs().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=hs,o=Cs();if(aa){if(void 0===n)throw Error(a(407));n=n()}else{if(n=t(),null===Ac)throw Error(a(349));0!=(30&gs)||Is(r,t,n)}o.memoizedState=n;var s={value:n,getSnapshot:t};return o.queue=s,Zs(Os.bind(null,r,s,e),[e]),r.flags|=2048,zs(9,Rs.bind(null,r,s,n,t),void 0,null),n},useId:function(){var e=Cs(),t=Ac.identifierPrefix;if(aa){var n=Xo;t=":"+t+"R"+(n=(Qo&~(1<<32-st(Qo)-1)).toString(32)+n),0<(n=_s++)&&(t+="H"+n.toString(32)),t+=":"}else t=":"+t+"r"+(n=ws++).toString(32)+":";return e.memoizedState=t},unstable_isNewReconciler:!1},ci={readContext:Ca,useCallback:Ys,useContext:Ca,useEffect:Hs,useImperativeHandle:Ws,useInsertionEffect:Gs,useLayoutEffect:qs,useMemo:Qs,useReducer:js,useRef:Bs,useState:function(){return js(Ps)},useDebugValue:Ks,useDeferredValue:function(e){return Xs(Ts(),ms.memoizedState,e)},useTransition:function(){return[js(Ps)[0],Ts().memoizedState]},useMutableSource:Ls,useSyncExternalStore:Ns,useId:ei,unstable_isNewReconciler:!1},li={readContext:Ca,useCallback:Ys,useContext:Ca,useEffect:Hs,useImperativeHandle:Ws,useInsertionEffect:Gs,useLayoutEffect:qs,useMemo:Qs,useReducer:As,useRef:Bs,useState:function(){return As(Ps)},useDebugValue:Ks,useDeferredValue:function(e){var t=Ts();return null===ms?t.memoizedState=e:Xs(t,ms.memoizedState,e)},useTransition:function(){return[As(Ps)[0],Ts().memoizedState]},useMutableSource:Ls,useSyncExternalStore:Ns,useId:ei,unstable_isNewReconciler:!1};function ui(e,t){try{var n="",r=t;do{n+=$(r),r=r.return}while(r);var o=n}catch(a){o="\nError generating stack: "+a.message+"\n"+a.stack}return{value:e,source:t,stack:o,digest:null}}function di(e,t,n){return{value:e,source:null,stack:null!=n?n:null,digest:null!=t?t:null}}function pi(e,t){try{console.error(t.value)}catch(n){setTimeout((function(){throw n}))}}var fi="function"==typeof WeakMap?WeakMap:Map;function gi(e,t,n){(n=Ra(-1,n)).tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Gc||(Gc=!0,qc=r),pi(0,t)},n}function hi(e,t,n){(n=Ra(-1,n)).tag=3;var r=e.type.getDerivedStateFromError;if("function"==typeof r){var o=t.value;n.payload=function(){return r(o)},n.callback=function(){pi(0,t)}}var a=e.stateNode;return null!==a&&"function"==typeof a.componentDidCatch&&(n.callback=function(){pi(0,t),"function"!=typeof r&&(null===Vc?Vc=new Set([this]):Vc.add(this));var e=t.stack;this.componentDidCatch(t.value,{componentStack:null!==e?e:""})}),n}function mi(e,t,n){var r=e.pingCache;if(null===r){r=e.pingCache=new fi;var o=new Set;r.set(t,o)}else void 0===(o=r.get(t))&&(o=new Set,r.set(t,o));o.has(n)||(o.add(n),e=Cl.bind(null,e,t,n),t.then(e,e))}function bi(e){do{var t;if((t=13===e.tag)&&(t=null===(t=e.memoizedState)||null!==t.dehydrated),t)return e;e=e.return}while(null!==e);return null}function vi(e,t,n,r,o){return 0==(1&e.mode)?(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,1===n.tag&&(null===n.alternate?n.tag=17:((t=Ra(-1,1)).tag=2,Oa(n,t,1))),n.lanes|=1),e):(e.flags|=65536,e.lanes=o,e)}var yi=_.ReactCurrentOwner,_i=!1;function wi(e,t,n,r){t.child=null===e?Xa(t,null,n,r):Qa(t,e.child,n,r)}function xi(e,t,n,r,o){n=n.render;var a=t.ref;return Ea(t,o),r=Ss(e,t,n,r,a,o),n=Es(),null===e||_i?(aa&&n&&ta(t),t.flags|=1,wi(e,t,r,o),t.child):(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,Gi(e,t,o))}function ki(e,t,n,r,o){if(null===e){var a=n.type;return"function"!=typeof a||Il(a)||void 0!==a.defaultProps||null!==n.compare||void 0!==n.defaultProps?((e=Ol(n.type,null,r,t,t.mode,o)).ref=t.ref,e.return=t,t.child=e):(t.tag=15,t.type=a,Si(e,t,a,r,o))}if(a=e.child,0==(e.lanes&o)){var s=a.memoizedProps;if((n=null!==(n=n.compare)?n:cr)(s,r)&&e.ref===t.ref)return Gi(e,t,o)}return t.flags|=1,(e=Rl(a,r)).ref=t.ref,e.return=t,t.child=e}function Si(e,t,n,r,o){if(null!==e){var a=e.memoizedProps;if(cr(a,r)&&e.ref===t.ref){if(_i=!1,t.pendingProps=r=a,0==(e.lanes&o))return t.lanes=e.lanes,Gi(e,t,o);0!=(131072&e.flags)&&(_i=!0)}}return Ti(e,t,n,r,o)}function Ei(e,t,n){var r=t.pendingProps,o=r.children,a=null!==e?e.memoizedState:null;if("hidden"===r.mode)if(0==(1&t.mode))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},To(Rc,Ic),Ic|=n;else{if(0==(1073741824&n))return e=null!==a?a.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,To(Rc,Ic),Ic|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=null!==a?a.baseLanes:n,To(Rc,Ic),Ic|=r}else null!==a?(r=a.baseLanes|n,t.memoizedState=null):r=n,To(Rc,Ic),Ic|=r;return wi(e,t,o,n),t.child}function Ci(e,t){var n=t.ref;(null===e&&null!==n||null!==e&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Ti(e,t,n,r,o){var a=Io(n)?Lo:jo.current;return a=No(t,a),Ea(t,o),n=Ss(e,t,n,r,a,o),r=Es(),null===e||_i?(aa&&r&&ta(t),t.flags|=1,wi(e,t,n,o),t.child):(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,Gi(e,t,o))}function Pi(e,t,n,r,o){if(Io(n)){var a=!0;Fo(t)}else a=!1;if(Ea(t,o),null===t.stateNode)Hi(e,t),Ha(t,n,r),qa(t,n,r,o),r=!0;else if(null===e){var s=t.stateNode,i=t.memoizedProps;s.props=i;var c=s.context,l=n.contextType;"object"==typeof l&&null!==l?l=Ca(l):l=No(t,l=Io(n)?Lo:jo.current);var u=n.getDerivedStateFromProps,d="function"==typeof u||"function"==typeof s.getSnapshotBeforeUpdate;d||"function"!=typeof s.UNSAFE_componentWillReceiveProps&&"function"!=typeof s.componentWillReceiveProps||(i!==r||c!==l)&&Ga(t,s,r,l),La=!1;var p=t.memoizedState;s.state=p,Da(t,r,s,o),c=t.memoizedState,i!==r||p!==c||Ao.current||La?("function"==typeof u&&($a(t,n,u,r),c=t.memoizedState),(i=La||Za(t,n,i,r,p,c,l))?(d||"function"!=typeof s.UNSAFE_componentWillMount&&"function"!=typeof s.componentWillMount||("function"==typeof s.componentWillMount&&s.componentWillMount(),"function"==typeof s.UNSAFE_componentWillMount&&s.UNSAFE_componentWillMount()),"function"==typeof s.componentDidMount&&(t.flags|=4194308)):("function"==typeof s.componentDidMount&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=c),s.props=r,s.state=c,s.context=l,r=i):("function"==typeof s.componentDidMount&&(t.flags|=4194308),r=!1)}else{s=t.stateNode,Ia(e,t),i=t.memoizedProps,l=t.type===t.elementType?i:ba(t.type,i),s.props=l,d=t.pendingProps,p=s.context,"object"==typeof(c=n.contextType)&&null!==c?c=Ca(c):c=No(t,c=Io(n)?Lo:jo.current);var f=n.getDerivedStateFromProps;(u="function"==typeof f||"function"==typeof s.getSnapshotBeforeUpdate)||"function"!=typeof s.UNSAFE_componentWillReceiveProps&&"function"!=typeof s.componentWillReceiveProps||(i!==d||p!==c)&&Ga(t,s,r,c),La=!1,p=t.memoizedState,s.state=p,Da(t,r,s,o);var g=t.memoizedState;i!==d||p!==g||Ao.current||La?("function"==typeof f&&($a(t,n,f,r),g=t.memoizedState),(l=La||Za(t,n,l,r,p,g,c)||!1)?(u||"function"!=typeof s.UNSAFE_componentWillUpdate&&"function"!=typeof s.componentWillUpdate||("function"==typeof s.componentWillUpdate&&s.componentWillUpdate(r,g,c),"function"==typeof s.UNSAFE_componentWillUpdate&&s.UNSAFE_componentWillUpdate(r,g,c)),"function"==typeof s.componentDidUpdate&&(t.flags|=4),"function"==typeof s.getSnapshotBeforeUpdate&&(t.flags|=1024)):("function"!=typeof s.componentDidUpdate||i===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),"function"!=typeof s.getSnapshotBeforeUpdate||i===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=g),s.props=r,s.state=g,s.context=c,r=l):("function"!=typeof s.componentDidUpdate||i===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),"function"!=typeof s.getSnapshotBeforeUpdate||i===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),r=!1)}return ji(e,t,n,r,a,o)}function ji(e,t,n,r,o,a){Ci(e,t);var s=0!=(128&t.flags);if(!r&&!s)return o&&Do(t,n,!1),Gi(e,t,a);r=t.stateNode,yi.current=t;var i=s&&"function"!=typeof n.getDerivedStateFromError?null:r.render();return t.flags|=1,null!==e&&s?(t.child=Qa(t,e.child,null,a),t.child=Qa(t,null,i,a)):wi(e,t,i,a),t.memoizedState=r.state,o&&Do(t,n,!0),t.child}function Ai(e){var t=e.stateNode;t.pendingContext?Oo(0,t.pendingContext,t.pendingContext!==t.context):t.context&&Oo(0,t.context,!1),os(e,t.containerInfo)}function Li(e,t,n,r,o){return ga(),ha(o),t.flags|=256,wi(e,t,n,r),t.child}var Ni,Ii,Ri,Oi,Mi={dehydrated:null,treeContext:null,retryLane:0};function Fi(e){return{baseLanes:e,cachePool:null,transitions:null}}function Di(e,t,n){var r,o=t.pendingProps,s=cs.current,i=!1,c=0!=(128&t.flags);if((r=c)||(r=(null===e||null!==e.memoizedState)&&0!=(2&s)),r?(i=!0,t.flags&=-129):null!==e&&null===e.memoizedState||(s|=1),To(cs,1&s),null===e)return ua(t),null!==(e=t.memoizedState)&&null!==(e=e.dehydrated)?(0==(1&t.mode)?t.lanes=1:"$!"===e.data?t.lanes=8:t.lanes=1073741824,null):(c=o.children,e=o.fallback,i?(o=t.mode,i=t.child,c={mode:"hidden",children:c},0==(1&o)&&null!==i?(i.childLanes=0,i.pendingProps=c):i=Fl(c,o,0,null),e=Ml(e,o,n,null),i.return=t,e.return=t,i.sibling=e,t.child=i,t.child.memoizedState=Fi(n),t.memoizedState=Mi,e):zi(t,c));if(null!==(s=e.memoizedState)&&null!==(r=s.dehydrated))return function(e,t,n,r,o,s,i){if(n)return 256&t.flags?(t.flags&=-257,Bi(e,t,i,r=di(Error(a(422))))):null!==t.memoizedState?(t.child=e.child,t.flags|=128,null):(s=r.fallback,o=t.mode,r=Fl({mode:"visible",children:r.children},o,0,null),(s=Ml(s,o,i,null)).flags|=2,r.return=t,s.return=t,r.sibling=s,t.child=r,0!=(1&t.mode)&&Qa(t,e.child,null,i),t.child.memoizedState=Fi(i),t.memoizedState=Mi,s);if(0==(1&t.mode))return Bi(e,t,i,null);if("$!"===o.data){if(r=o.nextSibling&&o.nextSibling.dataset)var c=r.dgst;return r=c,Bi(e,t,i,r=di(s=Error(a(419)),r,void 0))}if(c=0!=(i&e.childLanes),_i||c){if(null!==(r=Ac)){switch(i&-i){case 4:o=2;break;case 16:o=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:o=32;break;case 536870912:o=268435456;break;default:o=0}0!==(o=0!=(o&(r.suspendedLanes|i))?0:o)&&o!==s.retryLane&&(s.retryLane=o,Aa(e,o),rl(r,e,o,-1))}return ml(),Bi(e,t,i,r=di(Error(a(421))))}return"$?"===o.data?(t.flags|=128,t.child=e.child,t=Pl.bind(null,e),o._reactRetry=t,null):(e=s.treeContext,oa=lo(o.nextSibling),ra=t,aa=!0,sa=null,null!==e&&(Wo[Ko++]=Qo,Wo[Ko++]=Xo,Wo[Ko++]=Yo,Qo=e.id,Xo=e.overflow,Yo=t),t=zi(t,r.children),t.flags|=4096,t)}(e,t,c,o,r,s,n);if(i){i=o.fallback,c=t.mode,r=(s=e.child).sibling;var l={mode:"hidden",children:o.children};return 0==(1&c)&&t.child!==s?((o=t.child).childLanes=0,o.pendingProps=l,t.deletions=null):(o=Rl(s,l)).subtreeFlags=14680064&s.subtreeFlags,null!==r?i=Rl(r,i):(i=Ml(i,c,n,null)).flags|=2,i.return=t,o.return=t,o.sibling=i,t.child=o,o=i,i=t.child,c=null===(c=e.child.memoizedState)?Fi(n):{baseLanes:c.baseLanes|n,cachePool:null,transitions:c.transitions},i.memoizedState=c,i.childLanes=e.childLanes&~n,t.memoizedState=Mi,o}return e=(i=e.child).sibling,o=Rl(i,{mode:"visible",children:o.children}),0==(1&t.mode)&&(o.lanes=n),o.return=t,o.sibling=null,null!==e&&(null===(n=t.deletions)?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=o,t.memoizedState=null,o}function zi(e,t){return(t=Fl({mode:"visible",children:t},e.mode,0,null)).return=e,e.child=t}function Bi(e,t,n,r){return null!==r&&ha(r),Qa(t,e.child,null,n),(e=zi(t,t.pendingProps.children)).flags|=2,t.memoizedState=null,e}function $i(e,t,n){e.lanes|=t;var r=e.alternate;null!==r&&(r.lanes|=t),Sa(e.return,t,n)}function Ui(e,t,n,r,o){var a=e.memoizedState;null===a?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:o}:(a.isBackwards=t,a.rendering=null,a.renderingStartTime=0,a.last=r,a.tail=n,a.tailMode=o)}function Zi(e,t,n){var r=t.pendingProps,o=r.revealOrder,a=r.tail;if(wi(e,t,r.children,n),0!=(2&(r=cs.current)))r=1&r|2,t.flags|=128;else{if(null!==e&&0!=(128&e.flags))e:for(e=t.child;null!==e;){if(13===e.tag)null!==e.memoizedState&&$i(e,n,t);else if(19===e.tag)$i(e,n,t);else if(null!==e.child){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;null===e.sibling;){if(null===e.return||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(To(cs,r),0==(1&t.mode))t.memoizedState=null;else switch(o){case"forwards":for(n=t.child,o=null;null!==n;)null!==(e=n.alternate)&&null===ls(e)&&(o=n),n=n.sibling;null===(n=o)?(o=t.child,t.child=null):(o=n.sibling,n.sibling=null),Ui(t,!1,o,n,a);break;case"backwards":for(n=null,o=t.child,t.child=null;null!==o;){if(null!==(e=o.alternate)&&null===ls(e)){t.child=o;break}e=o.sibling,o.sibling=n,n=o,o=e}Ui(t,!0,n,null,a);break;case"together":Ui(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Hi(e,t){0==(1&t.mode)&&null!==e&&(e.alternate=null,t.alternate=null,t.flags|=2)}function Gi(e,t,n){if(null!==e&&(t.dependencies=e.dependencies),Fc|=t.lanes,0==(n&t.childLanes))return null;if(null!==e&&t.child!==e.child)throw Error(a(153));if(null!==t.child){for(n=Rl(e=t.child,e.pendingProps),t.child=n,n.return=t;null!==e.sibling;)e=e.sibling,(n=n.sibling=Rl(e,e.pendingProps)).return=t;n.sibling=null}return t.child}function qi(e,t){if(!aa)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;null!==t;)null!==t.alternate&&(n=t),t=t.sibling;null===n?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;null!==n;)null!==n.alternate&&(r=n),n=n.sibling;null===r?t||null===e.tail?e.tail=null:e.tail.sibling=null:r.sibling=null}}function Vi(e){var t=null!==e.alternate&&e.alternate.child===e.child,n=0,r=0;if(t)for(var o=e.child;null!==o;)n|=o.lanes|o.childLanes,r|=14680064&o.subtreeFlags,r|=14680064&o.flags,o.return=e,o=o.sibling;else for(o=e.child;null!==o;)n|=o.lanes|o.childLanes,r|=o.subtreeFlags,r|=o.flags,o.return=e,o=o.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Wi(e,t,n){var r=t.pendingProps;switch(na(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Vi(t),null;case 1:case 17:return Io(t.type)&&Ro(),Vi(t),null;case 3:return r=t.stateNode,as(),Co(Ao),Co(jo),ds(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),null!==e&&null!==e.child||(pa(t)?t.flags|=4:null===e||e.memoizedState.isDehydrated&&0==(256&t.flags)||(t.flags|=1024,null!==sa&&(il(sa),sa=null))),Ii(e,t),Vi(t),null;case 5:is(t);var o=rs(ns.current);if(n=t.type,null!==e&&null!=t.stateNode)Ri(e,t,n,r,o),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(null===t.stateNode)throw Error(a(166));return Vi(t),null}if(e=rs(es.current),pa(t)){r=t.stateNode,n=t.type;var s=t.memoizedProps;switch(r[fo]=t,r[go]=s,e=0!=(1&t.mode),n){case"dialog":zr("cancel",r),zr("close",r);break;case"iframe":case"object":case"embed":zr("load",r);break;case"video":case"audio":for(o=0;o <\/script>",e=e.removeChild(e.firstChild)):"string"==typeof r.is?e=c.createElement(n,{is:r.is}):(e=c.createElement(n),"select"===n&&(c=e,r.multiple?c.multiple=!0:r.size&&(c.size=r.size))):e=c.createElementNS(e,n),e[fo]=t,e[go]=r,Ni(e,t,!1,!1),t.stateNode=e;e:{switch(c=ye(n,r),n){case"dialog":zr("cancel",e),zr("close",e),o=r;break;case"iframe":case"object":case"embed":zr("load",e),o=r;break;case"video":case"audio":for(o=0;o Zc&&(t.flags|=128,r=!0,qi(s,!1),t.lanes=4194304)}else{if(!r)if(null!==(e=ls(c))){if(t.flags|=128,r=!0,null!==(n=e.updateQueue)&&(t.updateQueue=n,t.flags|=4),qi(s,!0),null===s.tail&&"hidden"===s.tailMode&&!c.alternate&&!aa)return Vi(t),null}else 2*Qe()-s.renderingStartTime>Zc&&1073741824!==n&&(t.flags|=128,r=!0,qi(s,!1),t.lanes=4194304);s.isBackwards?(c.sibling=t.child,t.child=c):(null!==(n=s.last)?n.sibling=c:t.child=c,s.last=c)}return null!==s.tail?(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=Qe(),t.sibling=null,n=cs.current,To(cs,r?1&n|2:1&n),t):(Vi(t),null);case 22:case 23:return pl(),r=null!==t.memoizedState,null!==e&&null!==e.memoizedState!==r&&(t.flags|=8192),r&&0!=(1&t.mode)?0!=(1073741824&Ic)&&(Vi(t),6&t.subtreeFlags&&(t.flags|=8192)):Vi(t),null;case 24:case 25:return null}throw Error(a(156,t.tag))}function Ki(e,t){switch(na(t),t.tag){case 1:return Io(t.type)&&Ro(),65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 3:return as(),Co(Ao),Co(jo),ds(),0!=(65536&(e=t.flags))&&0==(128&e)?(t.flags=-65537&e|128,t):null;case 5:return is(t),null;case 13:if(Co(cs),null!==(e=t.memoizedState)&&null!==e.dehydrated){if(null===t.alternate)throw Error(a(340));ga()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 19:return Co(cs),null;case 4:return as(),null;case 10:return ka(t.type._context),null;case 22:case 23:return pl(),null;default:return null}}Ni=function(e,t){for(var n=t.child;null!==n;){if(5===n.tag||6===n.tag)e.appendChild(n.stateNode);else if(4!==n.tag&&null!==n.child){n.child.return=n,n=n.child;continue}if(n===t)break;for(;null===n.sibling;){if(null===n.return||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}},Ii=function(){},Ri=function(e,t,n,r){var o=e.memoizedProps;if(o!==r){e=t.stateNode,rs(es.current);var a,s=null;switch(n){case"input":o=K(e,o),r=K(e,r),s=[];break;case"select":o=F({},o,{value:void 0}),r=F({},r,{value:void 0}),s=[];break;case"textarea":o=re(e,o),r=re(e,r),s=[];break;default:"function"!=typeof o.onClick&&"function"==typeof r.onClick&&(e.onclick=Jr)}for(u in ve(n,r),n=null,o)if(!r.hasOwnProperty(u)&&o.hasOwnProperty(u)&&null!=o[u])if("style"===u){var c=o[u];for(a in c)c.hasOwnProperty(a)&&(n||(n={}),n[a]="")}else"dangerouslySetInnerHTML"!==u&&"children"!==u&&"suppressContentEditableWarning"!==u&&"suppressHydrationWarning"!==u&&"autoFocus"!==u&&(i.hasOwnProperty(u)?s||(s=[]):(s=s||[]).push(u,null));for(u in r){var l=r[u];if(c=null!=o?o[u]:void 0,r.hasOwnProperty(u)&&l!==c&&(null!=l||null!=c))if("style"===u)if(c){for(a in c)!c.hasOwnProperty(a)||l&&l.hasOwnProperty(a)||(n||(n={}),n[a]="");for(a in l)l.hasOwnProperty(a)&&c[a]!==l[a]&&(n||(n={}),n[a]=l[a])}else n||(s||(s=[]),s.push(u,n)),n=l;else"dangerouslySetInnerHTML"===u?(l=l?l.__html:void 0,c=c?c.__html:void 0,null!=l&&c!==l&&(s=s||[]).push(u,l)):"children"===u?"string"!=typeof l&&"number"!=typeof l||(s=s||[]).push(u,""+l):"suppressContentEditableWarning"!==u&&"suppressHydrationWarning"!==u&&(i.hasOwnProperty(u)?(null!=l&&"onScroll"===u&&zr("scroll",e),s||c===l||(s=[])):(s=s||[]).push(u,l))}n&&(s=s||[]).push("style",n);var u=s;(t.updateQueue=u)&&(t.flags|=4)}},Oi=function(e,t,n,r){n!==r&&(t.flags|=4)};var Yi=!1,Qi=!1,Xi="function"==typeof WeakSet?WeakSet:Set,Ji=null;function ec(e,t){var n=e.ref;if(null!==n)if("function"==typeof n)try{n(null)}catch(r){El(e,t,r)}else n.current=null}function tc(e,t,n){try{n()}catch(r){El(e,t,r)}}var nc=!1;function rc(e,t,n){var r=t.updateQueue;if(null!==(r=null!==r?r.lastEffect:null)){var o=r=r.next;do{if((o.tag&e)===e){var a=o.destroy;o.destroy=void 0,void 0!==a&&tc(t,n,a)}o=o.next}while(o!==r)}}function oc(e,t){if(null!==(t=null!==(t=t.updateQueue)?t.lastEffect:null)){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function ac(e){var t=e.ref;if(null!==t){var n=e.stateNode;e.tag,e=n,"function"==typeof t?t(e):t.current=e}}function sc(e){var t=e.alternate;null!==t&&(e.alternate=null,sc(t)),e.child=null,e.deletions=null,e.sibling=null,5===e.tag&&(null!==(t=e.stateNode)&&(delete t[fo],delete t[go],delete t[mo],delete t[bo],delete t[vo])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ic(e){return 5===e.tag||3===e.tag||4===e.tag}function cc(e){e:for(;;){for(;null===e.sibling;){if(null===e.return||ic(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;5!==e.tag&&6!==e.tag&&18!==e.tag;){if(2&e.flags)continue e;if(null===e.child||4===e.tag)continue e;e.child.return=e,e=e.child}if(!(2&e.flags))return e.stateNode}}function lc(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?8===n.nodeType?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(8===n.nodeType?(t=n.parentNode).insertBefore(e,n):(t=n).appendChild(e),null!=(n=n._reactRootContainer)||null!==t.onclick||(t.onclick=Jr));else if(4!==r&&null!==(e=e.child))for(lc(e,t,n),e=e.sibling;null!==e;)lc(e,t,n),e=e.sibling}function uc(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(4!==r&&null!==(e=e.child))for(uc(e,t,n),e=e.sibling;null!==e;)uc(e,t,n),e=e.sibling}var dc=null,pc=!1;function fc(e,t,n){for(n=n.child;null!==n;)gc(e,t,n),n=n.sibling}function gc(e,t,n){if(at&&"function"==typeof at.onCommitFiberUnmount)try{at.onCommitFiberUnmount(ot,n)}catch(i){}switch(n.tag){case 5:Qi||ec(n,t);case 6:var r=dc,o=pc;dc=null,fc(e,t,n),pc=o,null!==(dc=r)&&(pc?(e=dc,n=n.stateNode,8===e.nodeType?e.parentNode.removeChild(n):e.removeChild(n)):dc.removeChild(n.stateNode));break;case 18:null!==dc&&(pc?(e=dc,n=n.stateNode,8===e.nodeType?co(e.parentNode,n):1===e.nodeType&&co(e,n),Ut(e)):co(dc,n.stateNode));break;case 4:r=dc,o=pc,dc=n.stateNode.containerInfo,pc=!0,fc(e,t,n),dc=r,pc=o;break;case 0:case 11:case 14:case 15:if(!Qi&&(null!==(r=n.updateQueue)&&null!==(r=r.lastEffect))){o=r=r.next;do{var a=o,s=a.destroy;a=a.tag,void 0!==s&&(0!=(2&a)||0!=(4&a))&&tc(n,t,s),o=o.next}while(o!==r)}fc(e,t,n);break;case 1:if(!Qi&&(ec(n,t),"function"==typeof(r=n.stateNode).componentWillUnmount))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(i){El(n,t,i)}fc(e,t,n);break;case 21:fc(e,t,n);break;case 22:1&n.mode?(Qi=(r=Qi)||null!==n.memoizedState,fc(e,t,n),Qi=r):fc(e,t,n);break;default:fc(e,t,n)}}function hc(e){var t=e.updateQueue;if(null!==t){e.updateQueue=null;var n=e.stateNode;null===n&&(n=e.stateNode=new Xi),t.forEach((function(t){var r=jl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))}))}}function mc(e,t){var n=t.deletions;if(null!==n)for(var r=0;r o&&(o=i),r&=~s}if(r=o,10<(r=(120>(r=Qe()-r)?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Ec(r/1960))-r)){e.timeoutHandle=ro(xl.bind(null,e,$c,Hc),r);break}xl(e,$c,Hc);break;default:throw Error(a(329))}}}return ol(e,Qe()),e.callbackNode===n?al.bind(null,e):null}function sl(e,t){var n=Bc;return e.current.memoizedState.isDehydrated&&(fl(e,t).flags|=256),2!==(e=bl(e,t))&&(t=$c,$c=n,null!==t&&il(t)),e}function il(e){null===$c?$c=e:$c.push.apply($c,e)}function cl(e,t){for(t&=~zc,t&=~Dc,e.suspendedLanes|=t,e.pingedLanes&=~t,e=e.expirationTimes;0 e?16:e,null===Kc)var r=!1;else{if(e=Kc,Kc=null,Yc=0,0!=(6&jc))throw Error(a(331));var o=jc;for(jc|=4,Ji=e.current;null!==Ji;){var s=Ji,i=s.child;if(0!=(16&Ji.flags)){var c=s.deletions;if(null!==c){for(var l=0;l Qe()-Uc?fl(e,0):zc|=n),ol(e,t)}function Tl(e,t){0===t&&(0==(1&e.mode)?t=1:(t=ut,0==(130023424&(ut<<=1))&&(ut=4194304)));var n=tl();null!==(e=Aa(e,t))&&(bt(e,t,n),ol(e,n))}function Pl(e){var t=e.memoizedState,n=0;null!==t&&(n=t.retryLane),Tl(e,n)}function jl(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;null!==o&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(a(314))}null!==r&&r.delete(t),Tl(e,n)}function Al(e,t){return Ve(e,t)}function Ll(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Nl(e,t,n,r){return new Ll(e,t,n,r)}function Il(e){return!(!(e=e.prototype)||!e.isReactComponent)}function Rl(e,t){var n=e.alternate;return null===n?((n=Nl(e.tag,t,e.key,e.mode)).elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=14680064&e.flags,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=null===t?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Ol(e,t,n,r,o,s){var i=2;if(r=e,"function"==typeof e)Il(e)&&(i=1);else if("string"==typeof e)i=5;else e:switch(e){case k:return Ml(n.children,o,s,t);case S:i=8,o|=8;break;case E:return(e=Nl(12,n,t,2|o)).elementType=E,e.lanes=s,e;case j:return(e=Nl(13,n,t,o)).elementType=j,e.lanes=s,e;case A:return(e=Nl(19,n,t,o)).elementType=A,e.lanes=s,e;case I:return Fl(n,o,s,t);default:if("object"==typeof e&&null!==e)switch(e.$$typeof){case C:i=10;break e;case T:i=9;break e;case P:i=11;break e;case L:i=14;break e;case N:i=16,r=null;break e}throw Error(a(130,null==e?e:typeof e,""))}return(t=Nl(i,n,t,o)).elementType=e,t.type=r,t.lanes=s,t}function Ml(e,t,n,r){return(e=Nl(7,e,r,t)).lanes=n,e}function Fl(e,t,n,r){return(e=Nl(22,e,r,t)).elementType=I,e.lanes=n,e.stateNode={isHidden:!1},e}function Dl(e,t,n){return(e=Nl(6,e,null,t)).lanes=n,e}function zl(e,t,n){return(t=Nl(4,null!==e.children?e.children:[],e.key,t)).lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Bl(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=mt(0),this.expirationTimes=mt(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=mt(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function $l(e,t,n,r,o,a,s,i,c){return e=new Bl(e,t,n,i,c),1===t?(t=1,!0===a&&(t|=8)):t=0,a=Nl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Na(a),e}function Ul(e){if(!e)return Po;e:{if(Ue(e=e._reactInternals)!==e||1!==e.tag)throw Error(a(170));var t=e;do{switch(t.tag){case 3:t=t.stateNode.context;break e;case 1:if(Io(t.type)){t=t.stateNode.__reactInternalMemoizedMergedChildContext;break e}}t=t.return}while(null!==t);throw Error(a(171))}if(1===e.tag){var n=e.type;if(Io(n))return Mo(e,n,t)}return t}function Zl(e,t,n,r,o,a,s,i,c){return(e=$l(n,r,!0,e,0,a,0,i,c)).context=Ul(null),n=e.current,(a=Ra(r=tl(),o=nl(n))).callback=null!=t?t:null,Oa(n,a,o),e.current.lanes=o,bt(e,o,r),ol(e,r),e}function Hl(e,t,n,r){var o=t.current,a=tl(),s=nl(o);return n=Ul(n),null===t.context?t.context=n:t.pendingContext=n,(t=Ra(a,s)).payload={element:e},null!==(r=void 0===r?null:r)&&(t.callback=r),null!==(e=Oa(o,t,s))&&(rl(e,o,s,a),Ma(e,o,s)),s}function Gl(e){return(e=e.current).child?(e.child.tag,e.child.stateNode):null}function ql(e,t){if(null!==(e=e.memoizedState)&&null!==e.dehydrated){var n=e.retryLane;e.retryLane=0!==n&&n {"use strict";var r=n(73935);t.createRoot=r.createRoot,t.hydrateRoot=r.hydrateRoot},73935:(e,t,n)=>{"use strict";!function e(){if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}(),e.exports=n(64448)},69590:e=>{var t="undefined"!=typeof Element,n="function"==typeof Map,r="function"==typeof Set,o="function"==typeof ArrayBuffer&&!!ArrayBuffer.isView;function a(e,s){if(e===s)return!0;if(e&&s&&"object"==typeof e&&"object"==typeof s){if(e.constructor!==s.constructor)return!1;var i,c,l,u;if(Array.isArray(e)){if((i=e.length)!=s.length)return!1;for(c=i;0!=c--;)if(!a(e[c],s[c]))return!1;return!0}if(n&&e instanceof Map&&s instanceof Map){if(e.size!==s.size)return!1;for(u=e.entries();!(c=u.next()).done;)if(!s.has(c.value[0]))return!1;for(u=e.entries();!(c=u.next()).done;)if(!a(c.value[1],s.get(c.value[0])))return!1;return!0}if(r&&e instanceof Set&&s instanceof Set){if(e.size!==s.size)return!1;for(u=e.entries();!(c=u.next()).done;)if(!s.has(c.value[0]))return!1;return!0}if(o&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(s)){if((i=e.length)!=s.length)return!1;for(c=i;0!=c--;)if(e[c]!==s[c])return!1;return!0}if(e.constructor===RegExp)return e.source===s.source&&e.flags===s.flags;if(e.valueOf!==Object.prototype.valueOf&&"function"==typeof e.valueOf&&"function"==typeof s.valueOf)return e.valueOf()===s.valueOf();if(e.toString!==Object.prototype.toString&&"function"==typeof e.toString&&"function"==typeof s.toString)return e.toString()===s.toString();if((i=(l=Object.keys(e)).length)!==Object.keys(s).length)return!1;for(c=i;0!=c--;)if(!Object.prototype.hasOwnProperty.call(s,l[c]))return!1;if(t&&e instanceof Element)return!1;for(c=i;0!=c--;)if(("_owner"!==l[c]&&"__v"!==l[c]&&"__o"!==l[c]||!e.$$typeof)&&!a(e[l[c]],s[l[c]]))return!1;return!0}return e!=e&&s!=s}e.exports=function(e,t){try{return a(e,t)}catch(n){if((n.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw n}}},70405:(e,t,n)=>{"use strict";n.d(t,{B6:()=>G,ql:()=>J});var r=n(67294),o=n(45697),a=n.n(o),s=n(69590),i=n.n(s),c=n(41143),l=n.n(c),u=n(96774),d=n.n(u);function p(){return p=Object.assign||function(e){for(var t=1;t =0||(o[n]=e[n]);return o}var m={BASE:"base",BODY:"body",HEAD:"head",HTML:"html",LINK:"link",META:"meta",NOSCRIPT:"noscript",SCRIPT:"script",STYLE:"style",TITLE:"title",FRAGMENT:"Symbol(react.fragment)"},b={rel:["amphtml","canonical","alternate"]},v={type:["application/ld+json"]},y={charset:"",name:["robots","description"],property:["og:type","og:title","og:url","og:image","og:image:alt","og:description","twitter:url","twitter:title","twitter:description","twitter:image","twitter:image:alt","twitter:card","twitter:site"]},_=Object.keys(m).map((function(e){return m[e]})),w={accesskey:"accessKey",charset:"charSet",class:"className",contenteditable:"contentEditable",contextmenu:"contextMenu","http-equiv":"httpEquiv",itemprop:"itemProp",tabindex:"tabIndex"},x=Object.keys(w).reduce((function(e,t){return e[w[t]]=t,e}),{}),k=function(e,t){for(var n=e.length-1;n>=0;n-=1){var r=e[n];if(Object.prototype.hasOwnProperty.call(r,t))return r[t]}return null},S=function(e){var t=k(e,m.TITLE),n=k(e,"titleTemplate");if(Array.isArray(t)&&(t=t.join("")),n&&t)return n.replace(/%s/g,(function(){return t}));var r=k(e,"defaultTitle");return t||r||void 0},E=function(e){return k(e,"onChangeClientState")||function(){}},C=function(e,t){return t.filter((function(t){return void 0!==t[e]})).map((function(t){return t[e]})).reduce((function(e,t){return p({},e,t)}),{})},T=function(e,t){return t.filter((function(e){return void 0!==e[m.BASE]})).map((function(e){return e[m.BASE]})).reverse().reduce((function(t,n){if(!t.length)for(var r=Object.keys(n),o=0;o /g,">").replace(/"/g,""").replace(/'/g,"'")},O=function(e){return Object.keys(e).reduce((function(t,n){var r=void 0!==e[n]?n+'="'+e[n]+'"':""+n;return t?t+" "+r:r}),"")},M=function(e,t){return void 0===t&&(t={}),Object.keys(e).reduce((function(t,n){return t[w[n]||n]=e[n],t}),t)},F=function(e,t){return t.map((function(t,n){var o,a=((o={key:n})["data-rh"]=!0,o);return Object.keys(t).forEach((function(e){var n=w[e]||e;"innerHTML"===n||"cssText"===n?a.dangerouslySetInnerHTML={__html:t.innerHTML||t.cssText}:a[n]=t[e]})),r.createElement(e,a)}))},D=function(e,t,n){switch(e){case m.TITLE:return{toComponent:function(){return n=t.titleAttributes,(o={key:e=t.title})["data-rh"]=!0,a=M(n,o),[r.createElement(m.TITLE,a,e)];var e,n,o,a},toString:function(){return function(e,t,n,r){var o=O(n),a=A(t);return o?"<"+e+' data-rh="true" '+o+">"+R(a,r)+""+e+">":"<"+e+' data-rh="true">'+R(a,r)+""+e+">"}(e,t.title,t.titleAttributes,n)}};case"bodyAttributes":case"htmlAttributes":return{toComponent:function(){return M(t)},toString:function(){return O(t)}};default:return{toComponent:function(){return F(e,t)},toString:function(){return function(e,t,n){return t.reduce((function(t,r){var o=Object.keys(r).filter((function(e){return!("innerHTML"===e||"cssText"===e)})).reduce((function(e,t){var o=void 0===r[t]?t:t+'="'+R(r[t],n)+'"';return e?e+" "+o:o}),""),a=r.innerHTML||r.cssText||"",s=-1===I.indexOf(e);return t+"<"+e+' data-rh="true" '+o+(s?"/>":">"+a+""+e+">")}),"")}(e,t,n)}}}},z=function(e){var t=e.baseTag,n=e.bodyAttributes,r=e.encode,o=e.htmlAttributes,a=e.noscriptTags,s=e.styleTags,i=e.title,c=void 0===i?"":i,l=e.titleAttributes,u=e.linkTags,d=e.metaTags,p=e.scriptTags,f={toComponent:function(){},toString:function(){return""}};if(e.prioritizeSeoTags){var g=function(e){var t=e.linkTags,n=e.scriptTags,r=e.encode,o=L(e.metaTags,y),a=L(t,b),s=L(n,v);return{priorityMethods:{toComponent:function(){return[].concat(F(m.META,o.priority),F(m.LINK,a.priority),F(m.SCRIPT,s.priority))},toString:function(){return D(m.META,o.priority,r)+" "+D(m.LINK,a.priority,r)+" "+D(m.SCRIPT,s.priority,r)}},metaTags:o.default,linkTags:a.default,scriptTags:s.default}}(e);f=g.priorityMethods,u=g.linkTags,d=g.metaTags,p=g.scriptTags}return{priority:f,base:D(m.BASE,t,r),bodyAttributes:D("bodyAttributes",n,r),htmlAttributes:D("htmlAttributes",o,r),link:D(m.LINK,u,r),meta:D(m.META,d,r),noscript:D(m.NOSCRIPT,a,r),script:D(m.SCRIPT,p,r),style:D(m.STYLE,s,r),title:D(m.TITLE,{title:c,titleAttributes:l},r)}},B=[],$=function(e,t){var n=this;void 0===t&&(t="undefined"!=typeof document),this.instances=[],this.value={setHelmet:function(e){n.context.helmet=e},helmetInstances:{get:function(){return n.canUseDOM?B:n.instances},add:function(e){(n.canUseDOM?B:n.instances).push(e)},remove:function(e){var t=(n.canUseDOM?B:n.instances).indexOf(e);(n.canUseDOM?B:n.instances).splice(t,1)}}},this.context=e,this.canUseDOM=t,t||(e.helmet=z({baseTag:[],bodyAttributes:{},encodeSpecialCharacters:!0,htmlAttributes:{},linkTags:[],metaTags:[],noscriptTags:[],scriptTags:[],styleTags:[],title:"",titleAttributes:{}}))},U=r.createContext({}),Z=a().shape({setHelmet:a().func,helmetInstances:a().shape({get:a().func,add:a().func,remove:a().func})}),H="undefined"!=typeof document,G=function(e){function t(n){var r;return(r=e.call(this,n)||this).helmetData=new $(r.props.context,t.canUseDOM),r}return f(t,e),t.prototype.render=function(){return r.createElement(U.Provider,{value:this.helmetData.value},this.props.children)},t}(r.Component);G.canUseDOM=H,G.propTypes={context:a().shape({helmet:a().shape()}),children:a().node.isRequired},G.defaultProps={context:{}},G.displayName="HelmetProvider";var q=function(e,t){var n,r=document.head||document.querySelector(m.HEAD),o=r.querySelectorAll(e+"[data-rh]"),a=[].slice.call(o),s=[];return t&&t.length&&t.forEach((function(t){var r=document.createElement(e);for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&("innerHTML"===o?r.innerHTML=t.innerHTML:"cssText"===o?r.styleSheet?r.styleSheet.cssText=t.cssText:r.appendChild(document.createTextNode(t.cssText)):r.setAttribute(o,void 0===t[o]?"":t[o]));r.setAttribute("data-rh","true"),a.some((function(e,t){return n=t,r.isEqualNode(e)}))?a.splice(n,1):s.push(r)})),a.forEach((function(e){return e.parentNode.removeChild(e)})),s.forEach((function(e){return r.appendChild(e)})),{oldTags:a,newTags:s}},V=function(e,t){var n=document.getElementsByTagName(e)[0];if(n){for(var r=n.getAttribute("data-rh"),o=r?r.split(","):[],a=[].concat(o),s=Object.keys(t),i=0;i =0;d-=1)n.removeAttribute(a[d]);o.length===a.length?n.removeAttribute("data-rh"):n.getAttribute("data-rh")!==s.join(",")&&n.setAttribute("data-rh",s.join(","))}},W=function(e,t){var n=e.baseTag,r=e.htmlAttributes,o=e.linkTags,a=e.metaTags,s=e.noscriptTags,i=e.onChangeClientState,c=e.scriptTags,l=e.styleTags,u=e.title,d=e.titleAttributes;V(m.BODY,e.bodyAttributes),V(m.HTML,r),function(e,t){void 0!==e&&document.title!==e&&(document.title=A(e)),V(m.TITLE,t)}(u,d);var p={baseTag:q(m.BASE,n),linkTags:q(m.LINK,o),metaTags:q(m.META,a),noscriptTags:q(m.NOSCRIPT,s),scriptTags:q(m.SCRIPT,c),styleTags:q(m.STYLE,l)},f={},g={};Object.keys(p).forEach((function(e){var t=p[e],n=t.newTags,r=t.oldTags;n.length&&(f[e]=n),r.length&&(g[e]=p[e].oldTags)})),t&&t(),i(e,f,g)},K=null,Y=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),o=0;o elements are self-closing and can not contain children. Refer to our API for more information.")}},n.flattenArrayTypeChildren=function(e){var t,n=e.child,r=e.arrayTypeChildren;return p({},r,((t={})[n.type]=[].concat(r[n.type]||[],[p({},e.newChildProps,this.mapNestedChildrenToProps(n,e.nestedChildren))]),t))},n.mapObjectTypeChildren=function(e){var t,n,r=e.child,o=e.newProps,a=e.newChildProps,s=e.nestedChildren;switch(r.type){case m.TITLE:return p({},o,((t={})[r.type]=s,t.titleAttributes=p({},a),t));case m.BODY:return p({},o,{bodyAttributes:p({},a)});case m.HTML:return p({},o,{htmlAttributes:p({},a)});default:return p({},o,((n={})[r.type]=p({},a),n))}},n.mapArrayTypeChildrenToProps=function(e,t){var n=p({},t);return Object.keys(e).forEach((function(t){var r;n=p({},n,((r={})[t]=e[t],r))})),n},n.warnOnInvalidChildren=function(e,t){return l()(_.some((function(t){return e.type===t})),"function"==typeof e.type?"You may be attempting to nest components within each other, which is not allowed. Refer to our API for more information.":"Only elements types "+_.join(", ")+" are allowed. Helmet does not support rendering <"+e.type+"> elements. Refer to our API for more information."),l()(!t||"string"==typeof t||Array.isArray(t)&&!t.some((function(e){return"string"!=typeof e})),"Helmet expects a string as a child of <"+e.type+">. Did you forget to wrap your children in braces? ( <"+e.type+">{``}"+e.type+"> ) Refer to our API for more information."),!0},n.mapChildrenToProps=function(e,t){var n=this,o={};return r.Children.forEach(e,(function(e){if(e&&e.props){var r=e.props,a=r.children,s=h(r,Q),i=Object.keys(s).reduce((function(e,t){return e[x[t]||t]=s[t],e}),{}),c=e.type;switch("symbol"==typeof c?c=c.toString():n.warnOnInvalidChildren(e,a),c){case m.FRAGMENT:t=n.mapChildrenToProps(a,t);break;case m.LINK:case m.META:case m.NOSCRIPT:case m.SCRIPT:case m.STYLE:o=n.flattenArrayTypeChildren({child:e,arrayTypeChildren:o,newChildProps:i,nestedChildren:a});break;default:t=n.mapObjectTypeChildren({child:e,newProps:t,newChildProps:i,nestedChildren:a})}}})),this.mapArrayTypeChildrenToProps(o,t)},n.render=function(){var e=this.props,t=e.children,n=h(e,X),o=p({},n),a=n.helmetData;return t&&(o=this.mapChildrenToProps(t,o)),!a||a instanceof $||(a=new $(a.context,a.instances)),a?r.createElement(Y,p({},o,{context:a.value,helmetData:void 0})):r.createElement(U.Consumer,null,(function(e){return r.createElement(Y,p({},o,{context:e}))}))},t}(r.Component);J.propTypes={base:a().object,bodyAttributes:a().object,children:a().oneOfType([a().arrayOf(a().node),a().node]),defaultTitle:a().string,defer:a().bool,encodeSpecialCharacters:a().bool,htmlAttributes:a().object,link:a().arrayOf(a().object),meta:a().arrayOf(a().object),noscript:a().arrayOf(a().object),onChangeClientState:a().func,script:a().arrayOf(a().object),style:a().arrayOf(a().object),title:a().string,titleAttributes:a().object,titleTemplate:a().string,prioritizeSeoTags:a().bool,helmetData:a().object},J.defaultProps={defer:!0,encodeSpecialCharacters:!0,prioritizeSeoTags:!1},J.displayName="Helmet"},69921:(e,t)=>{"use strict";var n="function"==typeof Symbol&&Symbol.for,r=n?Symbol.for("react.element"):60103,o=n?Symbol.for("react.portal"):60106,a=n?Symbol.for("react.fragment"):60107,s=n?Symbol.for("react.strict_mode"):60108,i=n?Symbol.for("react.profiler"):60114,c=n?Symbol.for("react.provider"):60109,l=n?Symbol.for("react.context"):60110,u=n?Symbol.for("react.async_mode"):60111,d=n?Symbol.for("react.concurrent_mode"):60111,p=n?Symbol.for("react.forward_ref"):60112,f=n?Symbol.for("react.suspense"):60113,g=n?Symbol.for("react.suspense_list"):60120,h=n?Symbol.for("react.memo"):60115,m=n?Symbol.for("react.lazy"):60116,b=n?Symbol.for("react.block"):60121,v=n?Symbol.for("react.fundamental"):60117,y=n?Symbol.for("react.responder"):60118,_=n?Symbol.for("react.scope"):60119;function w(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case r:switch(e=e.type){case u:case d:case a:case i:case s:case f:return e;default:switch(e=e&&e.$$typeof){case l:case p:case m:case h:case c:return e;default:return t}}case o:return t}}}function x(e){return w(e)===d}t.AsyncMode=u,t.ConcurrentMode=d,t.ContextConsumer=l,t.ContextProvider=c,t.Element=r,t.ForwardRef=p,t.Fragment=a,t.Lazy=m,t.Memo=h,t.Portal=o,t.Profiler=i,t.StrictMode=s,t.Suspense=f,t.isAsyncMode=function(e){return x(e)||w(e)===u},t.isConcurrentMode=x,t.isContextConsumer=function(e){return w(e)===l},t.isContextProvider=function(e){return w(e)===c},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===r},t.isForwardRef=function(e){return w(e)===p},t.isFragment=function(e){return w(e)===a},t.isLazy=function(e){return w(e)===m},t.isMemo=function(e){return w(e)===h},t.isPortal=function(e){return w(e)===o},t.isProfiler=function(e){return w(e)===i},t.isStrictMode=function(e){return w(e)===s},t.isSuspense=function(e){return w(e)===f},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===a||e===d||e===i||e===s||e===f||e===g||"object"==typeof e&&null!==e&&(e.$$typeof===m||e.$$typeof===h||e.$$typeof===c||e.$$typeof===l||e.$$typeof===p||e.$$typeof===v||e.$$typeof===y||e.$$typeof===_||e.$$typeof===b)},t.typeOf=w},59864:(e,t,n)=>{"use strict";e.exports=n(69921)},68356:(e,t,n)=>{"use strict";function r(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function o(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function s(){return s=Object.assign||function(e){for(var t=1;t {"use strict";n.d(t,{H:()=>i,f:()=>s});var r=n(16550),o=n(87462),a=n(67294);function s(e,t,n){return void 0===n&&(n=[]),e.some((function(e){var o=e.path?(0,r.LX)(t,e):n.length?n[n.length-1].match:r.F0.computeRootMatch(t);return o&&(n.push({route:e,match:o}),e.routes&&s(e.routes,t,n)),o})),n}function i(e,t,n){return void 0===t&&(t={}),void 0===n&&(n={}),e?a.createElement(r.rs,n,e.map((function(e,n){return a.createElement(r.AW,{key:e.key||n,path:e.path,exact:e.exact,strict:e.strict,render:function(n){return e.render?e.render((0,o.Z)({},n,{},t,{route:e})):a.createElement(e.component,(0,o.Z)({},n,t,{route:e}))}})}))):null}},73727:(e,t,n)=>{"use strict";n.d(t,{OL:()=>y,VK:()=>u,rU:()=>m});var r=n(16550),o=n(75068),a=n(67294),s=n(99318),i=n(87462),c=n(63366),l=n(38776),u=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),o=0;o {"use strict";n.d(t,{AW:()=>S,F0:()=>y,LX:()=>k,TH:()=>I,k6:()=>N,rs:()=>A,s6:()=>v});var r=n(75068),o=n(67294),a=n(45697),s=n.n(a),i=n(99318),c=n(38776),l=n(87462),u=n(39658),d=n.n(u),p=(n(59864),n(63366)),f=(n(8679),1073741823),g="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:void 0!==n.g?n.g:{};var h=o.createContext||function(e,t){var n,a,i="__create-react-context-"+function(){var e="__global_unique_id__";return g[e]=(g[e]||0)+1}()+"__",c=function(e){function n(){for(var t,n,r,o=arguments.length,a=new Array(o),s=0;s {var r=n(5826);e.exports=f,e.exports.parse=a,e.exports.compile=function(e,t){return i(a(e,t),t)},e.exports.tokensToFunction=i,e.exports.tokensToRegExp=p;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,r=[],a=0,s=0,i="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],p=n[1],f=n.index;if(i+=e.slice(s,f),s=f+d.length,p)i+=p[1];else{var g=e[s],h=n[2],m=n[3],b=n[4],v=n[5],y=n[6],_=n[7];i&&(r.push(i),i="");var w=null!=h&&null!=g&&g!==h,x="+"===y||"*"===y,k="?"===y||"*"===y,S=n[2]||u,E=b||v;r.push({name:m||a++,prefix:h||"",delimiter:S,optional:k,repeat:x,partial:w,asterisk:!!_,pattern:E?l(E):_?".*":"[^"+c(S)+"]+?"})}}return s {"use strict";var r=n(67294),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),s=Object.prototype.hasOwnProperty,i=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,c={key:!0,ref:!0,__self:!0,__source:!0};function l(e,t,n){var r,a={},l=null,u=null;for(r in void 0!==n&&(l=""+n),void 0!==t.key&&(l=""+t.key),void 0!==t.ref&&(u=t.ref),t)s.call(t,r)&&!c.hasOwnProperty(r)&&(a[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===a[r]&&(a[r]=t[r]);return{$$typeof:o,type:e,key:l,ref:u,props:a,_owner:i.current}}t.Fragment=a,t.jsx=l,t.jsxs=l},72408:(e,t)=>{"use strict";var n=Symbol.for("react.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),i=Symbol.for("react.provider"),c=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),u=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),f=Symbol.iterator;var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,m={};function b(e,t,n){this.props=e,this.context=t,this.refs=m,this.updater=n||g}function v(){}function y(e,t,n){this.props=e,this.context=t,this.refs=m,this.updater=n||g}b.prototype.isReactComponent={},b.prototype.setState=function(e,t){if("object"!=typeof e&&"function"!=typeof e&&null!=e)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},b.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},v.prototype=b.prototype;var _=y.prototype=new v;_.constructor=y,h(_,b.prototype),_.isPureReactComponent=!0;var w=Array.isArray,x=Object.prototype.hasOwnProperty,k={current:null},S={key:!0,ref:!0,__self:!0,__source:!0};function E(e,t,r){var o,a={},s=null,i=null;if(null!=t)for(o in void 0!==t.ref&&(i=t.ref),void 0!==t.key&&(s=""+t.key),t)x.call(t,o)&&!S.hasOwnProperty(o)&&(a[o]=t[o]);var c=arguments.length-2;if(1===c)a.children=r;else if(1