1(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[748],{8393:function(s,e,n){let a=n(7294),l=a.forwardRef(function({title:s,titleId:e,...n},l){return a.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true",ref:l,"aria-labelledby":e},n),s?a.createElement("title",{id:e},s):null,a.createElement("path",{d:"M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z"}),a.createElement("path",{d:"M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.43z"}))});s.exports=l},1714:function(s,e,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/posts/real-time-session-invalidation",function(){return n(7066)}])},7066:function(s,e,n){"use strict";n.r(e),n.d(e,{frontmatter:function(){return r},meta:function(){return t}});var a=n(5893),l=n(1151),i=n(1552),c=n(4701);let r=void 0,t={title:"Let's Build Real-time Session Invalidation",publishedOn:"May 10, 2020",description:"How to limit users to a single client or browser instance of your web app.",draft:!1},MDXLayout=s=>{let{children:e}=s;return(0,a.jsx)(i.S,{meta:t,children:e})};function _createMdxContent(s){let e=Object.assign({p:"p",img:"img",a:"a",ul:"ul",li:"li",pre:"pre",code:"code",span:"span",h1:"h1",ol:"ol",h2:"h2",blockquote:"blockquote"},(0,l.ah)(),s.components);return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(e.p,{children:(0,a.jsx)(e.img,{src:"/images/session-invalidation/spiderman.jpg",alt:"Image of spiderman pointing at his clone"})}),"\n",(0,a.jsx)(e.p,{children:(0,a.jsx)(e.a,{href:"https://github.com/robzhu/logged-out",children:"Demo Repo"})}),"\n",(0,a.jsx)(e.p,{children:"Some applications need to limit users to a single client or browser instance. This post covers how to build, improve, and scale this feature. We begin with a simple web app with two API endpoints:"}),"\n",(0,a.jsxs)(e.ul,{children:["\n",(0,a.jsx)(e.li,{children:"Users log in by sending their user ID in the user HTTP request header to the /login route. Here's an example request/response:"}),"\n"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-bash",children:["curl -H ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"user:user123"'})," localhost:9000/login\n{",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionId"'}),":",(0,a.jsx)(e.span,{className:"hljs-string",children:'"364rl8"'}),"}\n"]})}),"\n",(0,a.jsxs)(e.ul,{children:["\n",(0,a.jsx)(e.li,{children:'The user adds sessionid=364rl8 as an HTTP header for the route /api. If the session ID is valid, the server returns "authenticatedâ, if not, the server returns an error:'}),"\n"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-bash",children:["curl -H ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionid=364rl8"'})," localhost:9000/api\nauthenticated\n\ncurl -H ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionid=badSession"'})," localhost:9000/api\nerror: invalid session\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["Our example returns the session ID in the HTTP response body, but it's more common in practice to store the session ID as a cookie, where the server returns the ",(0,a.jsx)(e.code,{children:"Set-Cookie: sessionid=364rl8;"})," HTTP header. This causes the browser to automatically include the session ID in all subsequent requests to the same domain."]}),"\n",(0,a.jsx)(e.h1,{children:"1. The Simplest Solution"}),"\n",(0,a.jsx)(e.p,{children:"The simplest solution is to use a server-side session cache that generates and stores a session ID for each user ID."}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { generateSessionId } = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"require"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"./utils"'}),");\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," cors = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"require"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"cors"'}),");\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," app = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"require"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"express"'}),")().",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"use"}),"(",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"cors"}),"());\n \n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"PORT"})," = ",(0,a.jsx)(e.span,{className:"hljs-number",children:"9000"}),";\n",(0,a.jsx)(e.span,{className:"hljs-comment",children:"// this will totally scale, trust me"}),"\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," sessions = {};\n \napp.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"get"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"/login"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"req, res"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { user } = req.",(0,a.jsx)(e.span,{className:"hljs-property",children:"headers"}),";\n \n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (!user) {\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"status"}),"(",(0,a.jsx)(e.span,{className:"hljs-number",children:"400"}),").",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:"\"error: request must include the 'user' HTTP header\""}),");\n } ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"else"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," sessionId = ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"generateSessionId"}),"();\n sessions[user] = sessionId;\n \n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"({ sessionId });\n }\n});\n \napp.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"get"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"/api"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"req, res"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { sessionid } = req.",(0,a.jsx)(e.span,{className:"hljs-property",children:"headers"}),";\n \n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (!sessionid) {\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"status"}),"(",(0,a.jsx)(e.span,{className:"hljs-number",children:"401"}),").",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"error: no sessionId. Log in at /login"'}),");\n } ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"else"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Object"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"values"}),"(sessions).",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"includes"}),"(sessionid)) {\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"authenticated"'}),");\n } ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"else"})," {\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"status"}),"(",(0,a.jsx)(e.span,{className:"hljs-number",children:"401"}),").",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"error: invalid session."'}),");\n }\n }\n});\n \napp.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"listen"}),"(",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"PORT"}
1),", ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-variable language_",children:"console"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"log"}),"(",(0,a.jsxs)(e.span,{className:"hljs-string",children:["`server started on http://localhost:",(0,a.jsx)(e.span,{className:"hljs-subst",children:"${PORT}"}),"`"]}),");\n});\n"]})}),"\n",(0,a.jsx)(e.p,{children:"Whenever a user successfully logs in, the session ID will be overridden. Requests that include outdated session IDs will fail validation, causing the server to return an error. However, if the client does not make an API request, the user will not know that the session was invalidated."}),"\n",(0,a.jsx)(e.p,{children:"Ideally, we want a client-side function that can tell us when the session is no longer valid:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"logIn"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"userId, onSessionInvalidated"}),")\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["The ",(0,a.jsx)(e.code,{children:"logIn"})," function takes a callback function (as the second argument) that will be invoked whenever we detect that the session is no longer valid. We can implement this API in two ways: polling and server-push."]}),"\n",(0,a.jsx)(e.h1,{children:"2. Polling"}),"\n",(0,a.jsxs)(e.p,{children:["If the client polls the ",(0,a.jsx)(e.code,{children:"/api"})," endpoint, we can detect an invalid session without explicit user interaction."]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"logIn"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"userId, onSessionInvalidated"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," response = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"fetch"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"http://localhost:9000/login"'}),", {\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"headers"}),": {\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"user"}),": userId,\n },\n });\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { sessionId } = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," response.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"json"}),"();\n \n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"POLLING_INTERVAL"})," = ",(0,a.jsx)(e.span,{className:"hljs-number",children:"200"}),";\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," poll = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"setInterval"}),"(",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," () => {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," response = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"fetch"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"http://localhost:9000/api"'}),", {\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"headers"}),": {\n sessionId,\n },\n });\n \n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (response.",(0,a.jsx)(e.span,{className:"hljs-property",children:"status"})," !== ",(0,a.jsx)(e.span,{className:"hljs-number",children:"200"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-comment",children:"// non-200 status code means the token is invalid"}),"\n ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"clearTimeout"}),"(poll);\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"onSessionInvalidated"}),"();\n }\n }, ",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"POLLING_INTERVAL"}),");\n \n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," sessionId;\n}\n"]})}),"\n",(0,a.jsx)(e.p,{children:"However, polling forces us to make a trade-off between latency and efficie
1ncy. The shorter the polling interval, the more quickly we can detect a bad session at the cost of more wasted polls."}),"\n",(0,a.jsx)(e.h1,{children:"3. Server-Push"}),"\n",(0,a.jsx)(e.p,{children:"If we encounter bottlenecks with the polling solution, then our final solution is to maintain a persistent, bi-directional channel on which the server can tell connected clients when their sessions are invalidated. For this demo, we'll use Web Sockets. To host a Web Socket server, we use the ws package."}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," wss = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"WebSocket"}),".",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Server"}),"({ ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"port"}),": ",(0,a.jsx)(e.span,{className:"hljs-number",children:"9001"})," });\n\nwss.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"on"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"connection"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"ws"}),") =>"]})," {\n ws.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"on"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"message"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"data"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," request = ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"JSON"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"parse"}),"(data);\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (request.",(0,a.jsx)(e.span,{className:"hljs-property",children:"action"})," === ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"subscribeToSessionInvalidation"'}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { sessionId } = request.",(0,a.jsx)(e.span,{className:"hljs-property",children:"args"}),";\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"subscribeToSessionInvalidation"}),"(sessionId, ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," {\n ws.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(\n ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"JSON"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"stringify"}),"({\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"event"}),": ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionInvalidated"'}),",\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"args"}),": {\n sessionId,\n },\n })\n );\n });\n }\n });\n});\n"]})}),"\n",(0,a.jsx)(e.p,{children:"This code tells the server to listen for incoming Web Socket connections on port 9001. For each new connection, listen for messages and assume the following format:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:["{\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"action"}),": ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"action ID"'}),",\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"args"}),": {...}\n}\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["If the ",(0,a.jsx)(e.code,{children:"action"})," value is ",(0,a.jsx)(e.code,{children:'"subscribeToSessionInvalidation"'}),", notify that client whenever the specified session ID is invali
1dated. Note: this solution requires generating session IDs that are hard to guess."]}),"\n",(0,a.jsx)(e.p,{children:"We also need to update our logIn route handler to detect existing sessions and publish the invalidation event:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:["app.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"get"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"/login"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"req, res"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { user } = req.",(0,a.jsx)(e.span,{className:"hljs-property",children:"headers"}),";\n\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (!user) {\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"status"}),"(",(0,a.jsx)(e.span,{className:"hljs-number",children:"400"}),").",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:"\"error: request must include the 'user' HTTP header\""}),");\n } ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"else"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," existingSession = sessions[user];\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (existingSession) {\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"publishSessionInvalidation"}),"(existingSession);\n }\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," sessionId = ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"generateSessionId"}),"();\n sessions[user] = sessionId;\n\n res.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"({ sessionId });\n }\n});\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["Here's the code for ",(0,a.jsx)(e.code,{children:"subscribeToSessionInvalidation"})," and ",(0,a.jsx)(e.code,{children:"publishSessionInvalidation"}),":"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"EventEmitter"})," } = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"require"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"events"'}),");\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," sessionEvents = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"EventEmitter"}),"();\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"SESSION_INVALIDATED"})," = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"session_invalidated"'}),";\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"publishSessionInvalidation"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"sessionId"}),") {\n sessionEvents.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"emit"}),"(",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"SESSION_INVALIDATED"}),", sessionId);\n}\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"subscribeToSessionInvalidation"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"sessionId, callback"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"listener"})," = (",(0,a.jsx)(e.span,{className:"hljs-params",children:"invalidatedSessionId"}),") => {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (sessionId === invalidatedSessionId) {\n sessionEvents.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"removeListener"}),"(",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"SESSION_INVALIDATED"}),", listener);\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"callback"}),"();\n }\n };\n\n sessionEvents.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"addListener"}),"(",(0,a.jsx)(e.span,{className:"hljs-variable constant_",children:"SESSION_INVALIDATED"}),", listener);\n}\n\n",(0,a.jsx)(e.span,{className:"hljs-variable language_",children:"module"}),".",(0,a.jsx)(e.span,{className:"hljs-property",children:"exports"}
1)," = {\n publishSessionInvalidation,\n subscribeToSessionInvalidation,\n};\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["Now we are ready to update the client to use the ",(0,a.jsx)(e.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/WebSocket",children:"WebSocket DOM API"})," to replace our polling logic:"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"logIn"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"userId, onSessionInvalidated"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," response = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"fetch"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"http://localhost:9000/login"'}),", {\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"headers"}),": {\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"user"}),": userId,\n },\n });\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { sessionId } = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," response.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"json"}),"();\n\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," socket = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"WebSocket"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"ws://localhost:9001"'}),");\n socket.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"addEventListener"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"open"'}),", ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-variable language_",children:"console"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"log"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"connected."'}),");\n socket.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"addEventListener"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"message"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"{ data }"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," { event, args } = ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"JSON"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"parse"}),"(data);\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (event === ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionInvalidated"'}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-comment",children:"// args.sessionId should equal sessionId"}),"\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"onSessionInvalidated"}),"();\n }\n });\n socket.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"send"}),"(\n ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"JSON"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"stringify"}),"({\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"action"}),": ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"subscribeToSessionInvalidation"'}),",\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"args"}),": {\n sessionId,\n },\n })\n );\n });\n\n socket.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"addEventListener"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"error"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"error"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-variable language_",children:"console"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"error"}),"(error);\n });\n\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," sessionId;\n}\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["Load ",(0,a.jsx)(e.code,{children:"/push/index.html"})," in your browser, and try it out. You should now see some real-time session invali
1dation action."]}),"\n",(0,a.jsx)(e.h1,{children:"4. Scaling"}),"\n",(0,a.jsx)(e.p,{children:(0,a.jsx)(e.img,{src:"/images/session-invalidation/oreilly-scaling.png",alt:"Oreilly book on scaling"})}),"\n",(0,a.jsx)(e.p,{children:"I bet you noticed that this solution doesn't scale. To create a more scalable version, we need to make the following changes:"}),"\n",(0,a.jsxs)(e.ol,{children:["\n",(0,a.jsx)(e.li,{children:"Move the session cache to a scalable distributed cache"}),"\n",(0,a.jsx)(e.li,{children:"Move from event emitter to a scalable distributed pubsub system"}),"\n",(0,a.jsx)(e.li,{children:"Update the client to add retry logic on disconnect"}),"\n"]}),"\n",(0,a.jsxs)(e.p,{children:[(0,a.jsx)(e.a,{href:"https://redis.io",children:"Redis"})," satisfies requirements #1 and #2. If we need to scale Redis, we can deploy a ",(0,a.jsx)(e.a,{href:"https://redis.io/topics/cluster-tutorial",children:"Redis cluster"})," or we can use a hosted version of Redis, such as ",(0,a.jsx)(e.a,{href:"https://aws.amazon.com/elasticache/redis/",children:"Amazon ElastiCache"}),"."]}),"\n",(0,a.jsx)(e.h2,{children:"Redis as a remote session cache"}),"\n",(0,a.jsx)(e.p,{children:"First, let's spin up a redis instance. Assuming you have a docker host available:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsx)(e.code,{className:"hljs language-bash",children:"docker run -d -p 6739:6739 redis\n"})}),"\n",(0,a.jsxs)(e.p,{children:["Make sure that port 6739 is open if you're running this on a cloud VM. If you don't have a cloud VM, you can launch a ",(0,a.jsx)(e.a,{href:"https://aws.amazon.com/ec2/pricing/",children:"t2.micro instances on EC2"})," as part of the AWS free tier. Once your VM is launched, you can ",(0,a.jsx)(e.a,{href:"https://docs.docker.com/engine/install/",children:"install docker"}),"."]}),"\n",(0,a.jsxs)(e.p,{children:["There are ",(0,a.jsx)(e.a,{href:"https://joonasw.net/view/redis-cache-session-store",children:"many"})," ",(0,a.jsx)(e.a,{href:"https://redislabs.com/redis-enterprise/use-cases/session-management/",children:"articles"})," that discuss session caching with Redis; here's my approach, using the ",(0,a.jsx)(e.a,{href:"https://www.npmjs.com/package/redis",children:"redis npm package"}),":"]}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-comment",children:"// remoteCache.js"}),"\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," redis = ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"require"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"redis"'}),");\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionCacheKey"})," = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessions"'}),";\n\nclient = redis.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"createClient"}),"({\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"host"}),": process.",(0,a.jsx)(e.span,{className:"hljs-property",children:"env"}),".",(0,a.jsx)(e.span,{className:"hljs-property",children:"REDIS_HOST"}),"\n});\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"getSession"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"userId"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Promise"}),"(",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"resolve"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," client.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"hmget"}),"(",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionCacheKey"}),", userId, ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"err, res"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"resolve"}),"(res ? (",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Array"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"isArray"}),"(res) ? res[",(0,a.jsx)(e.span,{className:"hljs-number",children:"0"}),"] : res) : ",(0,a.jsx)(e.span,{className:"hljs-literal",children:"null"}),");\n });\n });\n}\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"putSession"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"userId, sessionId"}),") {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Promise"}),"(",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"resolve"}),") =>"]})," {\n client.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"hmset"}),"(",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionCacheKey"}),", userId, sessionId, ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"err, res"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"resolve"}),"(res ? (",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Array"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"isArray"}),"(res) ? res[",(0,a.jsx)(e.span,{className:"hljs-number",children:"0"}),"] : res) : ",(0,a.jsx)(e.span,{className:"hljs-literal",children:"null"}),");\n });\n });\n}\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["We use the Redis commands ",(0,a.jsx)(e.a,{href:"https://redis.io/commands/hmget",children:"HMGET"})," and ",(0,a.jsx)(e.a,{href:"https://redis.io/commands/hmset",children:"HMSET"}),' (HM stands for "hash map") to respectively read and write the tuple ',(0,a.jsx)(e.code,{children:"[user ID, session ID]"}),". That takes care of the session storage, we still need to replace event emitter with Redis. The ",(0,a.jsx)(e.a,{href:"https://www.npmjs.com/package/redis",children:"redis npm"})," docs state:"]}),"\n",(0,a.jsxs)(e.blockquote,{children:["\n",(0,a.jsxs)(e.p,{children:["When a client issues a ",(0,a.jsx)(e.code,{children:"SUBSCRIBE"})," or ",(0,a.jsx)(e.code,{children:"PSUBSCRIBE"}
1),', that connection is put into a "subscriber" mode. At that point, the only valid commands are those that modify the subscription set, and quit (also ping on some redis versions). When the subscription set is empty, the connection is put back into regular mode.']}),"\n"]}),"\n",(0,a.jsx)(e.p,{children:"So we need to create two Redis clients, one for general commands, the other for dedicated subscriber commands:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-JavaScript",children:[(0,a.jsx)(e.span,{className:"hljs-comment",children:"// remoteCache.js"}),"\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionInvalidationChannel"})," = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionInvalidation"'}),";\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," pendingCallbacks = {};\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"connect"}),"(",(0,a.jsx)(e.span,{className:"hljs-params"}),") {\n client = redis.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"createClient"}),"({\n ",(0,a.jsx)(e.span,{className:"hljs-attr",children:"host"}),": process.",(0,a.jsx)(e.span,{className:"hljs-property",children:"env"}),".",(0,a.jsx)(e.span,{className:"hljs-property",children:"REDIS_HOST"}),"\n });\n ",(0,a.jsx)(e.span,{className:"hljs-comment",children:'// the redis client we\'re using works in two modes "normal" and'}),"\n ",(0,a.jsx)(e.span,{className:"hljs-comment",children:'// "subscriber". So we duplicate a client here and use that'}),"\n ",(0,a.jsx)(e.span,{className:"hljs-comment",children:"// for our subscriptions."}),"\n subscriber = client.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"duplicate"}),"();\n\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"return"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Promise"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"all"}),"([\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Promise"}),"(",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"resolve"}),") =>"]})," {\n client.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"on"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"ready"'}),", ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"resolve"}),"());\n }),\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," ",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Promise"}),"(",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"resolve"}),") =>"]})," {\n subscriber.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"on"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"ready"'}),", ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," {\n subscriber.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"on"}),"(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"message"'}),", ",(0,a.jsxs)(e.span,{className:"hljs-function",children:["(",(0,a.jsx)(e.span,{className:"hljs-params",children:"channel, invalidatedSession"}),") =>"]})," {\n ",(0,a.jsx)(e.span,{className:"hljs-variable language_",children:"console"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"log"}),"(channel, invalidatedSession);\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"Object"}),".",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"keys"}),"(pendingCallbacks).",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"includes"}),"(invalidatedSession)) {\n pendingCallbacks[invalidatedSession]();\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"delete"})," pendingCallbacks[invalidatedSession];\n }\n });\n\n subscriber.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"subscribe"}),"(",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionInvalidationChannel"}
1),", ",(0,a.jsx)(e.span,{className:"hljs-function",children:"() =>"})," {\n ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"resolve"}),"();\n });\n });\n }),\n ]);\n}\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"publishSessionInvalidation"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"sessionId"}),") {\n client.",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"publish"}),"(",(0,a.jsx)(e.span,{className:"hljs-title class_",children:"SessionInvalidationChannel"}),", sessionId);\n}\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"function"})," ",(0,a.jsx)(e.span,{className:"hljs-title function_",children:"subscribeToSessionInvalidation"}),"(",(0,a.jsx)(e.span,{className:"hljs-params",children:"sessionId, callback"}),") {\n pendingCallbacks[sessionId] = callback;\n}\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["In the ",(0,a.jsx)(e.code,{children:"connect"})," function, we subscribe to the ",(0,a.jsx)(e.code,{children:'"sessionInvalidation"'})," channel. We publish to this channel when another module calls ",(0,a.jsx)(e.code,{children:"publishSessionInvalidation"}),"."]}),"\n",(0,a.jsx)(e.p,{children:"You can run the demo like so:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-bash",children:["git ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"clone"})," https://github.com/robzhu/logged-out \n",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"cd"})," logged-out/push-redis/server\nnpm i && node server.js\n"]})}),"\n",(0,a.jsxs)(e.p,{children:["Next, open ",(0,a.jsx)(e.code,{children:"/push-redis/index.html"})," in two browser tabs and you should be able to see the working demo."]}),"\n",(0,a.jsx)(e.h1,{children:"5. Native Client"}),"\n",(0,a.jsx)(e.p,{children:"Let's take a moment to consider example applications that need real-time session invalidation. A few that come to mind for me: games, streaming media clients, advanced finance applications (e.g. bloomberg terminal). Since these sorts of applications are often built as native clients, let's see how a .net client looks:"}),"\n",(0,a.jsx)(e.pre,{children:(0,a.jsxs)(e.code,{className:"hljs language-csharp",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," System;\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," System.Net.Http;\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," System.Threading.Tasks;\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," Newtonsoft.Json;\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," Websocket.Client;\n\n",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"static"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"class"})," ",(0,a.jsx)(e.span,{className:"hljs-title",children:"Program"}),"\n{\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"string"})," LoginEndpoint = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"http://localhost:9000/login"'}),";\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"const"})," ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"string"})," UserID = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"1234"'}),";\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"static"})," Uri WebSocketEndpoint = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," Uri(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"ws://localhost:9001"'}),");\n\n ",(0,a.jsxs)(e.span,{className:"hljs-function",children:[(0,a.jsx)(e.span,{className:"hljs-keyword",children:"static"})," ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"async"})," Task ",(0,a.jsx)(e.span,{className:"hljs-title",children:"Main"}),"(",(0,a.jsxs)(e.span,{className:"hljs-params",children:[(0,a.jsx)(e.span,{className:"hljs-built_in",children:"string"}),"[] args"]}),")"]}),"\n {\n HttpClient client = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," HttpClient();\n\n client.DefaultRequestHeaders.Add(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"user"'}),", UserID);\n ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"dynamic"})," response = JsonConvert.DeserializeObject(",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," client.GetStringAsync(LoginEndpoint));\n ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"string"})," sessionId = response.sessionId;\n Console.WriteLine(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"Obtained session ID: "'})," + sessionId);\n\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"using"})," (",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"var"})," socket = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"})," WebsocketClient(WebSocketEndpoint))\n {\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"await"})," socket.Start();
1\n\n socket.MessageReceived.Subscribe(msg =>\n {\n ",(0,a.jsx)(e.span,{className:"hljs-built_in",children:"dynamic"})," payload = JsonConvert.DeserializeObject(msg.Text);\n ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"if"})," (payload[",(0,a.jsx)(e.span,{className:"hljs-string",children:'"event"'}),"] == ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"sessionInvalidated"'}),")\n {\n Console.WriteLine(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"You have logged in elsewhere. Exiting."'}),");\n Environment.Exit(",(0,a.jsx)(e.span,{className:"hljs-number",children:"0"}),");\n }\n });\n\n socket.Send(JsonConvert.SerializeObject(",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"}),"\n {\n action = ",(0,a.jsx)(e.span,{className:"hljs-string",children:'"subscribeToSessionInvalidation"'}),",\n args = ",(0,a.jsx)(e.span,{className:"hljs-keyword",children:"new"}),"\n {\n sessionId = sessionId\n }\n }));\n\n Console.WriteLine(",(0,a.jsx)(e.span,{className:"hljs-string",children:'"Press ENTER to exit."'}),");\n Console.ReadLine();\n }\n }\n}\n"]})}),"\n",(0,a.jsx)(c.Y,{children:"You can run the .net client and web client side by side and watch them invalidate one another."}),"\n",(0,a.jsx)(e.p,{children:"Of the many rough edges in the demo, the lack of type safety around the API stands out to me. Specifically, the topic names and the schema for the subscription request and response. Scaling this solution beyond one developer would require comprehensive documentation or a client-server type system, like a GraphQL schema."}),"\n",(0,a.jsx)(e.p,{children:"Over the course of building this demo, people have suggested several other solutions:"}),"\n",(0,a.jsxs)(e.ul,{children:["\n",(0,a.jsxs)(e.li,{children:[(0,a.jsx)(e.a,{href:"https://swr.now.sh/#focus-revalidation",children:"SWR"})," (",(0,a.jsx)(e.a,{href:"https://twitter.com/pacocoursey/status/1259579240023855104",children:"thanks @pacocoursey"}),")"]}),"\n",(0,a.jsxs)(e.li,{children:["Pubsub-as-a-service: ",(0,a.jsx)(e.a,{href:"https://pusher.com/",children:"pusher"})," and ",(0,a.jsx)(e.a,{href:"https://www.pubnub.com/",children:"pubnub"})]}),"\n",(0,a.jsx)(e.li,{children:(0,a.jsx)(e.a,{href:"https://updateloop.dev/websockets-api-gateway/",children:"Web Sockets with API Gateway"})}),"\n",(0,a.jsx)(e.li,{children:(0,a.jsx)(e.a,{href:"https://graphql.org/blog/2015-10-16-subscriptions/",children:"GraphQL Subscriptions"})}),"\n"]}),"\n",(0,a.jsx)(e.p,{children:"I hope this article gave you some ideas for building real-time session invalidation."})]})}e.default=function(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(0,a.jsx)(MDXLayout,Object.assign({},s,{children:(0,a.jsx)(_createMdxContent,s)}))}},4701:function(s,e,n){"use strict";n.d(e,{Y:function(){return Caption}});var a=n(5893);function Caption(s){let{children:e}=s;return(0,a.jsx)("div",{className:"mx-auto text-sm text-center text-gray-600",children:e})}},6048:function(s,e,n){"use strict";n.d(e,{C:function(){return Circle}});var a=n(5893),l=n(1664),i=n.n(l);function Circle(s){let{href:e,children:n,newTab:l}=s;return(0,a.jsx)(i(),{className:"max-w-lg relative p-3 rounded-full bg-white/50 hover:bg-white/80 backdrop-blur-md border-2 border-white ",href:e,target:l?"_blank":"",children:n})}},1552:function(s,e,n){"use strict";n.d(e,{S:function(){return Post}});var a=n(5893),l=n(7399),i=n.n(l),c=n(9008),r=n.n(c),t=n(6048),h=n(8393);function Post(s){let{children:e,meta:n}=s,l="The Update Loop: ".concat(n.title);return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(r(),{children:[(0,a.jsx)("title",{children:l}),(0,a.jsx)("meta",{name:"description",content:n.description}),(0,a.jsx)("link",{rel:"icon",href:"/favicon.ico"})]}),(0,a.jsxs)("main",{className:"".concat(i().className," post\n flex min-h-screen flex-col items-center \n my-12 lg:max-w-4xl mx-auto\n "),children:[(0,a.jsx)(t.C,{href:"/",children:(0,a.jsx)(h,{className:"w-8 h-8 text-[#92D6E3]"})}),(0,a.jsx)("h1",{className:"select-none mt-4",children:n.title}),(0,a.jsx)("p",{children:n.publishedOn}),(0,a.jsx)("div",{className:"text-left mx-8",children:e})]})]})}},7399:function(s){s.exports={style:{fontFamily:"'__Inter_e8ce0c', '__Inter_Fallback_e8ce0c'",fontStyle:"normal"},className:"__className_e8ce0c"}},1151:function(s,e,n){"use strict";n.d(e,{ah:function(){return useMDXComponents}});var a=n(7294);let l=a.createContext({});function useMDXComponents(s){let e=a.useContext(l);return a.useMemo(()=>"function"==typeof s?s(e):{...e,...s},[e,s])}}},function(s){s.O(0,[996,774,888,179],function(){return s(s.s=1714)}),_N_E=s.O()}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.