1"use strict";(self.webpackChunksample_website=self.webpackChunksample_website||[]).push([[1699],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>h});var r=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 s(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function l(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?s(Object(n),!0).forEach((function(t){a(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):s(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,r,a=function(e,t){if(null==e)return{};var n,r,a={},s=Object.keys(e);for(r=0;r<s.length;r++)n=s[r],t.indexOf(n)>=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(r=0;r<s.length;r++)n=s[r],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var i=r.createContext({}),c=function(e){var t=r.useContext(i),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},u=function(e){var t=c(e.components);return r.createElement(i.Provider,{value:t},e.children)},m="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,s=e.originalType,i=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),m=c(n),d=a,h=m["".concat(i,".").concat(d)]||m[d]||p[d]||s;return n?r.createElement(h,l(l({ref:t},u),{},{components:n})):r.createElement(h,l({ref:t},u))}));function h(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var s=n.length,l=new Array(s);l[0]=d;var o={};for(var i in t)hasOwnProperty.call(t,i)&&(o[i]=t[i]);o.originalType=e,o[m]="string"==typeof e?e:a,l[1]=o;for(var c=2;c<s;c++)l[c]=n[c];return r.createElement.apply(null,l)}return r.createElement.apply(null,n)}d.displayName="MDXCreateElement"},5162:(e,t,n)=>{n.d(t,{Z:()=>l});var r=n(7294),a=n(4334);const s={tabItem:"tabItem_Ymn6"};function l(e){let{children:t,hidden:n,className:l}=e;return r.createElement("div",{role:"tabpanel",className:(0,a.Z)(s.tabItem,l),hidden:n},t)}},4866:(e,t,n)=>{n.d(t,{Z:()=>T});var r=n(3117),a=n(7294),s=n(4334),l=n(2466),o=n(6550),i=n(1980),c=n(7392),u=n(12);function m(e){return function(e){return a.Children.map(e,(e=>{if(!e||(0,a.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad <Tabs> child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:r,default:a}}=e;return{value:t,label:n,attributes:r,default:a}}))}function p(e){const{values:t,children:n}=e;return(0,a.useMemo)((()=>{const e=t??m(n);return function(e){const t=(0,c.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[t,n])}function d(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function h(e){let{queryString:t=!1,groupId:n}=e;const r=(0,o.k6)(),s=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,i._X)(s),(0,a.useCallback)((e=>{if(!s)return;const t=new URLSearchParams(r.location.search);t.set(s,e),r.replace({...r.location,search:t.toString()})}),[s,r])]}function f(e){const{defaultValue:t,queryString:n=!1,groupId:r}=e,s=p(e),[l,o]=(0,a.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(t){if(!d({value:t,tabValues:n}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const r=n.find((e=>e.default))??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:t,tabValues:s}))),[i,c]=h({queryString:n,groupId:r}),[m,f]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[r,s]=(0,u.Nk)(n);return[r,(0,a.useCallback)((e=>{n&&s.set(e)}),[n,s])]}({groupId:r}),g=(()=>{const e=i??m;return d({value:e,tabValues:s})?e:null})();(0,a.useLayoutEffect)((()=>{g&&o(g)}),[g]);return{selectedValue:l,selectValue:(0,a.useCallback)((e=>{if(!d({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);o(e),c(e),f(e)}),[c,f,s]),tabValues:s}}var g=n(2389);const v={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function b(e){let{className:t,block:n,selectedValue:o,selectValue:i,tabValues:c}=e;const u=[],{blockElementScrollPositionUntilNextRender:m}=(0,l.o5)(),p=e=>{const t=e.currentTarget,n=u.indexOf(t),r=c[n].value;r!==o&&(m(t),i(r))},d=e=>{let t=null;switch(e.key){case"Enter":p(e);break;case"ArrowRight":{const n=u.indexOf(e.currentTarget)+1;t=u[n]??u[0];break}case"ArrowLeft":{const n=u.indexOf(e.currentTarget)-1;t=u[n]??u[u.length-1];break}}t?.focus()};return a.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.Z)("tabs",{"tabs--block":n},t)},c.map((e=>{let{value:t,label:n,attributes:l}=e;return a.createElement("li",(0,r.Z)({role:"tab",tabIndex:o===t?0:-1,"aria-selected":o===t,key:t,ref:e=>u.push(e),onKeyDown:d,onClick:p},l,{className:(0,s.Z)("tabs__item",v.tabItem,l?.className,{"tabs__item--active":o===t})}),n??t)})))}function y(e){let{lazy:t,children:n,selectedValue:r}=e;
1const s=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=s.find((e=>e.props.value===r));return e?(0,a.cloneElement)(e,{className:"margin-top--md"}):null}return a.createElement("div",{className:"margin-top--md"},s.map(((e,t)=>(0,a.cloneElement)(e,{key:t,hidden:e.props.value!==r}))))}function k(e){const t=f(e);return a.createElement("div",{className:(0,s.Z)("tabs-container",v.tabList)},a.createElement(b,(0,r.Z)({},e,t)),a.createElement(y,(0,r.Z)({},e,t)))}function T(e){const t=(0,g.Z)();return a.createElement(k,(0,r.Z)({key:String(t)},e))}},3936:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>u,contentTitle:()=>i,default:()=>h,frontMatter:()=>o,metadata:()=>c,toc:()=>m});var r=n(3117),a=(n(7294),n(3905)),s=n(4866),l=n(5162);const o={title:"Tutorial step #7 - Server delivery",sidebar_label:"Step #7: Server delivery",slug:"step-7"},i="Server delivery",c={unversionedId:"tutorial/server-delivery",id:"tutorial/server-delivery",title:"Tutorial step #7 - Server delivery",description:"There are two common ways to synchronize the state of the client upon reconnection:",source:"@site/docs/tutorial/10-server-delivery.md",sourceDirName:"tutorial",slug:"/tutorial/step-7",permalink:"/docs/v4/tutorial/step-7",draft:!1,editUrl:"https://github.com/socketio/socket.io-website/edit/main/docs/tutorial/10-server-delivery.md",tags:[],version:"current",lastUpdatedAt:1784117104,formattedLastUpdatedAt:"Jul 15, 2026",sidebarPosition:10,frontMatter:{title:"Tutorial step #7 - Server delivery",sidebar_label:"Step #7: Server delivery",slug:"step-7"},sidebar:"tutorialSidebar",previous:{title:"Step #6: Connection state recovery",permalink:"/docs/v4/tutorial/step-6"},next:{title:"Step #8: Client delivery",permalink:"/docs/v4/tutorial/step-8"}},u={},m=[],p={toc:m},d="wrapper";function h(e){let{components:t,...n}=e;return(0,a.kt)(d,(0,r.Z)({},p,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"server-delivery"},"Server delivery"),(0,a.kt)("p",null,"There are two common ways to synchronize the state of the client upon reconnection:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"either the server sends the whole state"),(0,a.kt)("li",{parentName:"ul"},"or the client keeps track of the last event it has processed and the server sends the missing pieces")),(0,a.kt)("p",null,"Both are totally valid solutions and choosing one will depend on your use case. In this tutorial, we will go with the latter."),(0,a.kt)("p",null,"First, let's persist the messages of our chat application. Today there are plenty of great options, we will use ",(0,a.kt)("a",{parentName:"p",href:"https://www.sqlite.org/"},"SQLite")," here."),(0,a.kt)("admonition",{type:"tip"},(0,a.kt)("p",{parentName:"admonition"},"If you are not familiar with SQLite, there are plenty of tutorials available online, like ",(0,a.kt)("a",{parentName:"p",href:"https://www.sqlitetutorial.net/"},"this one"),".")),(0,a.kt)("p",null,"Let's install the necessary packages:"),(0,a.kt)(s.Z,{groupId:"pm",mdxType:"Tabs"},(0,a.kt)(l.Z,{value:"npm",label:"NPM",default:!0,mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sh"},"npm install sqlite sqlite3\n"))),(0,a.kt)(l.Z,{value:"yarn",label:"Yarn",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sh"},"yarn add sqlite sqlite3\n"))),(0,a.kt)(l.Z,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sh"},"pnpm add sqlite sqlite3\n"))),(0,a.kt)(l.Z,{value:"bun",label:"Bun",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sh"},"bun add sqlite sqlite3\n")))),(0,a.kt)("p",null,"We will simply store each message in a SQL table:"),(0,a.kt)(s.Z,{groupId:"lang",mdxType:"Tabs"},(0,a.kt)(l.Z,{value:"cjs",label:"CommonJS",default:!0,mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="index.js"',title:'"index.js"'},"const express = require('express');\nconst { createServer } = require('node:http');\nconst { join } = require('node:path');\nconst { Server } = require('socket.io');\n// highlight-start\nconst sqlite3 = require('sqlite3');\nconst { open } = require('sqlite');\n// highlight-end\n\nasync function main() {\n // highlight-start\n // open the database file\n const db = await open({\n filename: 'chat.db',\n driver: sqlite3.Database\n });\n\n // create our 'messages' table (you can ignore the 'client_offset' column for now)\n await db.exec(`\n CREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n client_offset TEXT UNIQUE,\n content TEXT\n );\n `);\n // highlight-end\n\n const app = express();\n const server = createServer(app);\n const io = new Server(server, {\n connectionStateRecovery: {}\n });\n\n app.get('/', (req, res) => {\n res.sendFile(join(__dirname, 'index.html'));\n });\n\n io.on('connection', (socket) => {\n socket.on('chat message', async (msg) => {\n // highlight-start\n let result;
1\n try {\n // store the message in the database\n result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);\n } catch (e) {\n // TODO handle the failure\n return;\n }\n // include the offset with the message\n io.emit('chat message', msg, result.lastID);\n // highlight-end\n });\n });\n\n server.listen(3000, () => {\n console.log('server running at http://localhost:3000');\n });\n}\n\nmain();\n"))),(0,a.kt)(l.Z,{value:"mjs",label:"ES modules",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="index.js"',title:'"index.js"'},"import express from 'express';\nimport { createServer } from 'node:http';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\nimport { Server } from 'socket.io';\n// highlight-start\nimport sqlite3 from 'sqlite3';\nimport { open } from 'sqlite';\n\n// open the database file\nconst db = await open({\n filename: 'chat.db',\n driver: sqlite3.Database\n});\n\n// create our 'messages' table (you can ignore the 'client_offset' column for now)\nawait db.exec(`\n CREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n client_offset TEXT UNIQUE,\n content TEXT\n );\n`);\n// highlight-end\n\nconst app = express();\nconst server = createServer(app);\nconst io = new Server(server, {\n connectionStateRecovery: {}\n});\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\napp.get('/', (req, res) => {\n res.sendFile(join(__dirname, 'index.html'));\n});\n\nio.on('connection', (socket) => {\n socket.on('chat message', async (msg) => {\n // highlight-start\n let result;\n try {\n // store the message in the database\n result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);\n } catch (e) {\n // TODO handle the failure\n return;\n }\n // include the offset with the message\n io.emit('chat message', msg, result.lastID);\n // highlight-end\n });\n});\n\nserver.listen(3000, () => {\n console.log('server running at http://localhost:3000');\n});\n")))),(0,a.kt)("p",null,"The client will then keep track of the offset:"),(0,a.kt)(s.Z,{groupId:"syntax",mdxType:"Tabs"},(0,a.kt)(l.Z,{value:"es6",label:"ES6",default:!0,mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-html",metastring:'title="index.html"',title:'"index.html"'},"<script>\n // highlight-start\n const socket = io({\n auth: {\n serverOffset: 0\n }\n });\n // highlight-end\n\n const form = document.getElementById('form');\n const input = document.getElementById('input');\n const messages = document.getElementById('messages');\n\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n if (input.value) {\n socket.emit('chat message', input.value);\n input.value = '';\n }\n });\n\n // highlight-next-line\n socket.on('chat message', (msg, serverOffset) => {\n const item = document.createElement('li');\n item.textContent = msg;\n messages.appendChild(item);\n window.scrollTo(0, document.body.scrollHeight);\n // highlight-next-line\n socket.auth.serverOffset = serverOffset;\n });\n<\/script>\n"))),(0,a.kt)(l.Z,{value:"es5",label:"ES5",mdxType:"TabItem"},(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-html",metastring:'title="index.html"',title:'"index.html"'},"<script>\n // highlight-start\n var socket = io({\n auth: {\n serverOffset: 0\n }\n });\n // highlight-end\n\n var form = document.getElementById('form');\n var input = document.getElementById('input');\n var messages = document.getElementById('messages');\n\n form.addEventListener('submit', function(e) {\n e.preventDefault();\n if (input.value) {\n socket.emit('chat message', input.value);\n input.value = '';\n }\n });\n\n // highlight-next-line\n socket.on('chat message', function(msg, serverOffset) {\n var item = document.createElement('li');\n item.textContent = msg;\n messages.appendChild(item);\n window.scrollTo(0, document.body.scrollHeight);\n // highlight-next-line\n socket.auth.serverOffset = serverOffset;\n });\n<\/script>\n")))),(0,a.kt)("p",null,"And finally the server will send the missing messages upon (re)connection:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js",metastring:'title="index.js"',title:'"index.js"'},"// [...]\n\nio.on('connection', async (socket) => {\n socket.on('chat message', async (msg) => {\n let result;\n try {\n result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);\n } catch (e) {\n // TODO handle the failure\n return;\n }\n io.emit('chat message', msg, result.lastID);\n });
1\n\n // highlight-start\n if (!socket.recovered) {\n // if the connection state recovery was not successful\n try {\n await db.each('SELECT id, content FROM messages WHERE id > ?',\n [socket.handshake.auth.serverOffset || 0],\n (_err, row) => {\n socket.emit('chat message', row.content, row.id);\n }\n )\n } catch (e) {\n // something went wrong\n }\n }\n // highlight-end\n});\n\n// [...]\n")),(0,a.kt)("p",null,"Let's see it in action:"),(0,a.kt)("video",{controls:!0,width:"100%"},(0,a.kt)("source",{src:"/videos/tutorial/server-delivery.mp4"})),(0,a.kt)("p",null,"As you can see in the video above, it works both after a temporary disconnection and a full page refresh."),(0,a.kt)("admonition",{type:"tip"},(0,a.kt)("p",{parentName:"admonition"},'The difference with the "Connection state recovery" feature is that a successful recovery might not need to hit your main database (it might fetch the messages from a Redis stream for example).')),(0,a.kt)("p",null,"OK, now let's talk about the client delivery."))}h.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.