PageSourceSearch

https://socket.io/assets/js/87065066.bee36992.js

js socket.io collected 2026-09-24 07:24:25 UTC 15,392 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunksample_website=self.webpackChunksample_website||[]).push([[2175],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>m});var s=n(7294);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 r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,s)}return n}function i(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach((function(t){a(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function o(e,t){if(null==e)return{};var n,s,a=function(e,t){if(null==e)return{};var n,s,a={},r=Object.keys(e);for(s=0;s<r.length;s++)n=r[s],t.indexOf(n)>=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(s=0;s<r.length;s++)n=r[s],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=s.createContext({}),c=function(e){var t=s.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},p=function(e){var t=c(e.components);return s.createElement(l.Provider,{value:t},e.children)},u="mdxType",h={inlineCode:"code",wrapper:function(e){var t=e.children;return s.createElement(s.Fragment,{},t)}},d=s.forwardRef((function(e,t){var n=e.components,a=e.mdxType,r=e.originalType,l=e.parentName,p=o(e,["components","mdxType","originalType","parentName"]),u=c(n),d=a,m=u["".concat(l,".").concat(d)]||u[d]||h[d]||r;return n?s.createElement(m,i(i({ref:t},p),{},{components:n})):s.createElement(m,i({ref:t},p))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var r=n.length,i=new Array(r);i[0]=d;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[u]="string"==typeof e?e:a,i[1]=o;for(var c=2;c<r;c++)i[c]=n[c];return s.createElement.apply(null,i)}return s.createElement.apply(null,n)}d.displayName="MDXCreateElement"},7667:(e,t,n)=>{n.r(t),n.d(t,{contentTitle:()=>i,default:()=>u,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var s=n(3117),a=(n(7294),n(3905));const r={title:"Private messaging - Part II"},i="Private messaging - Part II",o={type:"mdx",permalink:"/get-started/private-messaging-part-2",source:"@site/src/pages/get-started/private-messaging-part-2.md",title:"Private messaging - Part II",description:"This guide has four distinct parts:",frontMatter:{title:"Private messaging - Part II"}},l=[{value:"Installation",id:"installation",level:2},{value:"How it works",id:"how-it-works",level:2},{value:"Persistent session ID",id:"persistent-session-id",level:3},{value:"Private messaging (updated)",id:"private-messaging-updated",level:3},{value:"Disconnection handler",id:"disconnection-handler",level:3},{value:"Review",id:"review",level:2}],c={toc:l},p="wrapper";
1function u(e){let{components:t,...n}=e;return(0,a.kt)(p,(0,s.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"private-messaging---part-ii"},"Private messaging - Part II"),(0,a.kt)("p",null,"This guide has four distinct parts:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/get-started/private-messaging-part-1/"},"Part I"),": initial implementation"),(0,a.kt)("li",{parentName:"ul"},"Part II ",(0,a.kt)("strong",{parentName:"li"},"(current)"),": persistent user ID"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/get-started/private-messaging-part-3/"},"Part III"),": persistent messages"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"/get-started/private-messaging-part-4/"},"Part IV"),": scaling up")),(0,a.kt)("p",null,"Here's where we were at the end of the ",(0,a.kt)("a",{parentName:"p",href:"/get-started/private-messaging-part-1/"},"1st part"),":"),(0,a.kt)("img",{src:"/images/private-messaging-part-1-chat.gif",alt:"Chat"}),(0,a.kt)("p",null,"Exchanging private messages is currently based on the ",(0,a.kt)("a",{parentName:"p",href:"/docs/v4/server-socket-instance/#Socket-id"},(0,a.kt)("inlineCode",{parentName:"a"},"socket.id"))," attribute, which works well but is problematic here because this ID is only valid for the current Socket.IO session and will change every time the low-level connection between the client and the server is severed."),(0,a.kt)("p",null,"So, every time the user reconnects, a new user will be created:"),(0,a.kt)("img",{src:"/images/private-messaging-part-1-duplicate-users.gif",alt:"Duplicate users"}),(0,a.kt)("p",null,"Which is... not that great. Let's fix this!"),(0,a.kt)("h2",{id:"installation"},"Installation"),(0,a.kt)("p",null,"Let's checkout the branch for part II:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"git checkout examples/private-messaging-part-2\n")),(0,a.kt)("p",null,"Here's what you should see in the current directory:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"\u251c\u2500\u2500 babel.config.js\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 public\n\u2502   \u251c\u2500\u2500 favicon.ico\n\u2502   \u251c\u2500\u2500 fonts\n\u2502   \u2502   \u2514\u2500\u2500 Lato-Regular.ttf\n\u2502   \u2514\u2500\u2500 index.html\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 server\n\u2502   \u251c\u2500\u2500 index.js (updated)\n\u2502   \u251c\u2500\u2500 package.json\n\u2502   \u2514\u2500\u2500 sessionStore.js (created)\n\u2514\u2500\u2500 src\n    \u251c\u2500\u2500 App.vue (updated)\n    \u251c\u2500\u2500 components\n    \u2502   \u251c\u2500\u2500 Chat.vue (updated)\n    \u2502   \u251c\u2500\u2500 MessagePanel.vue\n    \u2502   \u251c\u2500\u2500 SelectUsername.vue\n    \u2502   \u251c\u2500\u2500 StatusIcon.vue\n    \u2502   \u2514\u2500\u2500 User.vue\n    \u251c\u2500\u2500 main.js\n    \u2514\u2500\u2500 socket.js\n")),(0,a.kt)("p",null,"The complete diff can be found ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/socketio/socket.io/compare/examples/private-messaging-part-1...examples/private-messaging-part-2"},"here"),"."),(0,a.kt)("h2",{id:"how-it-works"},"How it works"),(0,a.kt)("h3",{id:"persistent-session-id"},"Persistent session ID"),(0,a.kt)("p",null,"On the server-side (",(0,a.kt)("inlineCode",{parentName:"p"},"server/index.js"),"), we create two random values:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"a session ID, private, which will be used to authenticate the user upon reconnection"),(0,a.kt)("li",{parentName:"ul"},"a user ID, public, which will be used as an identifier to exchange messages")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'io.use((socket, next) => {\n  const sessionID = socket.handshake.auth.sessionID;\n  if (sessionID) {\n    // find existing session\n    const session = sessionStore.findSession(sessionID);\n    if (session) {\n      socket.sessionID = sessionID;\n      socket.userID = session.userID;\n      socket.username = session.username;\n      return next();\n    }\n  }\n  const username = socket.handshake.auth.username;\n  if (!username) {\n    return next(new Error("invalid username"));\n  }\n  // create new session\n  socket.sessionID = randomId();\n  socket.userID = randomId();\n  socket.username = username;\n  next();\n});\n')),(0,a.kt)("p",null,"The session details are then sent to the user:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'io.on("connection", (socket) => {\n  // ...\n  socket.emit("session", {\n    sessionID: socket.sessionID,\n    userID: socket.userID,\n  });\n  // ...\n});\n')),(0,a.kt)("p",null,"On the client-side (",(0,a.kt)("inlineCode",{parentName:"p"},"src/App.vue"),"), we store the session ID in the ",(0,a.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"},"localStorage"),":"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"}
1,'socket.on("session", ({ sessionID, userID }) => {\n  // attach the session ID to the next reconnection attempts\n  socket.auth = { sessionID };\n  // store it in the localStorage\n  localStorage.setItem("sessionID", sessionID);\n  // save the ID of the user\n  socket.userID = userID;\n});\n')),(0,a.kt)("p",null,"Actually, there were several possible implementations:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"no storage at all: reconnection will preserve the session, but refreshing the page will lose it"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"},"sessionStorage"),": reconnection & refreshing the page will preserve the session"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"},"localStorage"),": reconnection & refreshing the page will preserve the session + this session will be shared across the browser tabs")),(0,a.kt)("p",null,"Here, we chose the ",(0,a.kt)("inlineCode",{parentName:"p"},"localStorage")," option, so all your tabs will be linked to the same session ID, which means that:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"you can chat with yourself (yay!)"),(0,a.kt)("li",{parentName:"ul"},"you now need to use another browser (or the private mode of your browser) to create another peer")),(0,a.kt)("p",null,"And finally, we fetch the session ID on application startup:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'created() {\n  const sessionID = localStorage.getItem("sessionID");\n\n  if (sessionID) {\n    this.usernameAlreadySelected = true;\n    socket.auth = { sessionID };\n    socket.connect();\n  }\n  // ...\n}\n')),(0,a.kt)("p",null,"You should now be able to refresh your tab without losing your session:"),(0,a.kt)("img",{src:"/images/private-messaging-part-2-persistent-session.gif",alt:"Persistent sessions"}),(0,a.kt)("p",null,"On the server-side, the session is saved in an in-memory store (",(0,a.kt)("inlineCode",{parentName:"p"},"server/sessionStore.js"),"):"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"class InMemorySessionStore extends SessionStore {\n  constructor() {\n    super();\n    this.sessions = new Map();\n  }\n\n  findSession(id) {\n    return this.sessions.get(id);\n  }\n\n  saveSession(id, session) {\n    this.sessions.set(id, session);\n  }\n\n  findAllSessions() {\n    return [...this.sessions.values()];\n  }\n}\n")),(0,a.kt)("p",null,"Again, this will only work with a single Socket.IO server, we'll come back to this in the 4th part of this guide."),(0,a.kt)("h3",{id:"private-messaging-updated"},"Private messaging (updated)"),(0,a.kt)("p",null,"The private messaging is now based on the ",(0,a.kt)("inlineCode",{parentName:"p"},"userID")," which is generated on the server-side, so we need to do two things:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"make the Socket instance join the associated room:")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'io.on("connection", (socket) => {\n  // ...\n  socket.join(socket.userID);\n  // ...\n});\n')),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"update the forwarding handler:")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'io.on("connection", (socket) => {\n  // ...\n  socket.on("private message", ({ content, to }) => {\n    socket.to(to).to(socket.userID).emit("private message", {\n      content,\n      from: socket.userID,\n      to,\n    });\n  });\n  // ...\n});\n')),(0,a.kt)("p",null,"Here's what happens:"),(0,a.kt)("img",{src:"/images/private-messaging-part-2-private-messaging.png",alt:"Private messaging"}),(0,a.kt)("p",null,"With ",(0,a.kt)("inlineCode",{parentName:"p"},"socket.to(to).to(socket.userID).emit(...)"),", we broadcast in both the recipient and the sender (excluding the given Socket instance) ",(0,a.kt)("a",{parentName:"p",href:"/docs/v4/rooms/"},"rooms"),"."),(0,a.kt)("p",null,"So now we have:"),(0,a.kt)("img",{src:"/images/private-messaging-part-2-chat.gif",alt:"Chat (v2)"}),(0,a.kt)("h3",{id:"disconnection-handler"},"Disconnection handler"),(0,a.kt)("p",null,"On the server-side, the Socket instance emits two special events: ",(0,a.kt)("a",{parentName:"p",href:"/docs/v4/server-socket-instance/#disconnecting"},"disconnecting")," and ",(0,a.kt)("a",{parentName:"p",href:"/docs/v4/server-socket-instance/#disconnect"},"disconnect")),(0,a.kt)("p",null,'We need to update our "disconnect" handler, because the session can now be shared across tabs:'),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},'io.on("connection", (socket) => {\n  // ...\n  socket.on("disconnect", async () => {\n    const matchingSockets = await io.in(socket.userID).allSockets();\n    const isDisconnected = matchingSockets.size === 0;\n    if (isDisconnected) {\n      // notify other users\n      socket.broadcast.emit("user disconnected", socket.userID);\n      // update the connection status of the session\n      sessionStore.saveSession(socket.sessionID, {\n        userID: socket.userID,\n        username: socket.username,\n        connected: false,\n      });\n    }\n  });\n});\n')),(0,a.kt)("p",null,"The ",(0,a.kt)("inlineCode",{parentName:"p"},"allSockets()")," method returns a Set containing the ID of all Socket instances that are in the given room."),(0,a.kt)("p",null,"Note: we could also have used the ",(0,a.kt)("inlineCode",{parentName:"p"},'io.of("/").sockets')," object, like in part I, but the ",(0,a.kt)("inlineCode",{parentName:"p"},"allSockets()")," method also works with multiple Socket.IO servers, which will be useful when scaling up."),(0,a.kt)("p",null,"Documentation: ",(0,a.kt)("a",{parentName:"p",href:"/docs/v4/server-api/#namespace-allSockets"},"allSockets() method")),(0,a.kt)("h2",{id:"review"},"Review"),(0,a.kt)("p",null,"OK, so\u2026 what we have now is better, but there is yet another issue: the messages are not actually persisted on the server. As a consequence, when the user reloads the page, it loses all its existing conversations."),(0,a.kt)("p",null,"This could be fixed for example by saving the messages in the localStorage of the browser, but there is another more annoying repercussion:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"when the sender gets disconnected, all the packets it sends are ",(0,a.kt)("a",{parentName:"li",href:"/docs/v4/client-offline-behavior/#Buffered-events"},"buffered")," until reconnection (which is great, in most cases)")),(0,a.kt)("img",{src:"/images/private-messaging-part-2-sender-offline.gif",alt:"Chat with sender that gets disconnected"}),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"but when the recipient gets disconnected, the packets are lost, since there is no listening Socket instance in the given room")),(0,a.kt)("img",{src:"/images/private-messaging-part-2-recipient-offline.gif",alt:"Chat with recipient that gets disconnected"}),(0,a.kt)("p",null,"We will try to fix this in the ",(0,a.kt)("a",{href:"/get-started/private-messaging-part-3/"},"3rd part")," of this guide."),(0,a.kt)("p",null,"Thanks for reading!"))}u.isMDXComponent=!0}}]);

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.