Repository: socketio/socket.io Branch: main Commit: e4d016bd5b4d Files: 756 Total size: 8.1 MB Directory structure: gitextract_cbdandz_/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build-examples.yml │ ├── ci-browser.yml │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ ├── README.md │ ├── engine.io-protocol/ │ │ ├── v3-test-suite/ │ │ │ ├── .gitignore │ │ │ ├── index.html │ │ │ ├── node-imports.js │ │ │ ├── package.json │ │ │ └── test-suite.js │ │ ├── v3.md │ │ ├── v4-current.md │ │ └── v4-test-suite/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── node-imports.js │ │ ├── package.json │ │ └── test-suite.js │ └── socket.io-protocol/ │ ├── v3.md │ ├── v4.md │ ├── v5-current.md │ └── v5-test-suite/ │ ├── .gitignore │ ├── index.html │ ├── node-imports.js │ ├── package.json │ └── test-suite.js ├── examples/ │ ├── .gitignore │ ├── ReactNativeExample/ │ │ ├── .bundle/ │ │ │ └── config │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .prettierrc.js │ │ ├── .watchmanconfig │ │ ├── App.tsx │ │ ├── Gemfile │ │ ├── README.md │ │ ├── __tests__/ │ │ │ └── App.test.tsx │ │ ├── android/ │ │ │ ├── app/ │ │ │ │ ├── build.gradle │ │ │ │ ├── debug.keystore │ │ │ │ ├── proguard-rules.pro │ │ │ │ └── src/ │ │ │ │ ├── debug/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── reactnativeexample/ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ └── values/ │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── build.gradle │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── gradlew │ │ │ ├── gradlew.bat │ │ │ └── settings.gradle │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.js │ │ ├── ios/ │ │ │ ├── .xcode.env │ │ │ ├── Podfile │ │ │ ├── ReactNativeExample/ │ │ │ │ ├── AppDelegate.h │ │ │ │ ├── AppDelegate.mm │ │ │ │ ├── Images.xcassets/ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Info.plist │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── main.m │ │ │ ├── ReactNativeExample.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── ReactNativeExample.xcscheme │ │ │ └── ReactNativeExampleTests/ │ │ │ ├── Info.plist │ │ │ └── ReactNativeExampleTests.m │ │ ├── jest.config.js │ │ ├── metro.config.js │ │ ├── package.json │ │ ├── server/ │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── socket.js │ │ └── tsconfig.json │ ├── angular-todomvc/ │ │ ├── .browserslistrc │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── e2e/ │ │ │ ├── protractor.conf.js │ │ │ ├── src/ │ │ │ │ ├── app.e2e-spec.ts │ │ │ │ └── app.po.ts │ │ │ └── tsconfig.json │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── server.ts │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app.component.css │ │ │ │ ├── app.component.html │ │ │ │ ├── app.component.spec.ts │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ └── store.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ ├── styles.css │ │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.spec.json │ │ └── tslint.json │ ├── basic-crud-application/ │ │ ├── README.md │ │ ├── angular-client/ │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.css │ │ │ │ │ ├── app.component.html │ │ │ │ │ ├── app.component.spec.ts │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ └── store.ts │ │ │ │ ├── assets/ │ │ │ │ │ └── .gitkeep │ │ │ │ ├── environments/ │ │ │ │ │ ├── environment.development.ts │ │ │ │ │ └── environment.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ ├── common/ │ │ │ └── events.ts │ │ ├── server/ │ │ │ ├── lib/ │ │ │ │ ├── app.ts │ │ │ │ ├── index.ts │ │ │ │ ├── todo-management/ │ │ │ │ │ ├── todo.handlers.ts │ │ │ │ │ └── todo.repository.ts │ │ │ │ └── util.ts │ │ │ ├── package.json │ │ │ ├── test/ │ │ │ │ └── todo-management/ │ │ │ │ └── todo.tests.ts │ │ │ └── tsconfig.json │ │ ├── server-postgres-cluster/ │ │ │ ├── README.md │ │ │ ├── docker-compose.yml │ │ │ ├── lib/ │ │ │ │ ├── app.js │ │ │ │ ├── cluster.js │ │ │ │ ├── index.js │ │ │ │ ├── todo-management/ │ │ │ │ │ ├── todo.handlers.js │ │ │ │ │ └── todo.repository.js │ │ │ │ └── util.js │ │ │ └── package.json │ │ └── vue-client/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── jsconfig.json │ │ ├── package.json │ │ ├── public/ │ │ │ ├── index.html │ │ │ └── styles.css │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.js │ │ │ ├── socket.js │ │ │ └── stores/ │ │ │ └── todo.js │ │ └── vue.config.js │ ├── basic-websocket-client/ │ │ ├── README.md │ │ ├── check-bundle-size.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ └── index.js │ │ └── test/ │ │ └── index.js │ ├── chat/ │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── public/ │ │ ├── index.html │ │ ├── main.js │ │ └── style.css │ ├── cluster-engine-node-cluster/ │ │ ├── README.md │ │ ├── client.js │ │ ├── package.json │ │ └── server.js │ ├── cluster-engine-redis/ │ │ ├── README.md │ │ ├── client.js │ │ ├── compose.yaml │ │ ├── package.json │ │ └── server.js │ ├── cluster-haproxy/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── haproxy.cfg │ │ └── server/ │ │ ├── Dockerfile │ │ ├── index.js │ │ ├── package.json │ │ └── public/ │ │ ├── index.html │ │ ├── main.js │ │ └── style.css │ ├── cluster-httpd/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── httpd.conf │ │ └── server/ │ │ ├── Dockerfile │ │ ├── index.js │ │ ├── package.json │ │ └── public/ │ │ ├── index.html │ │ ├── main.js │ │ └── style.css │ ├── cluster-nginx/ │ │ ├── README.md │ │ ├── client/ │ │ │ ├── Dockerfile │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── docker-compose.yml │ │ ├── nginx.conf │ │ └── server/ │ │ ├── Dockerfile │ │ ├── index.js │ │ ├── package.json │ │ └── public/ │ │ ├── index.html │ │ ├── main.js │ │ └── style.css │ ├── cluster-traefik/ │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── server/ │ │ │ ├── Dockerfile │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── public/ │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ └── style.css │ │ └── traefik.yml │ ├── connection-state-recovery-example/ │ │ ├── README.md │ │ ├── cjs/ │ │ │ ├── .codesandbox/ │ │ │ │ ├── Dockerfile │ │ │ │ └── tasks.json │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── package.json │ │ └── esm/ │ │ ├── .codesandbox/ │ │ │ ├── Dockerfile │ │ │ └── tasks.json │ │ ├── index.html │ │ ├── index.js │ │ └── package.json │ ├── create-react-app-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ ├── index.html │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ ├── server.js │ │ └── src/ │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── serviceWorker.js │ │ └── setupTests.js │ ├── custom-parsers/ │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ ├── .gitignore │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── client1.js │ │ │ ├── client2.js │ │ │ ├── client3.js │ │ │ ├── client4.js │ │ │ ├── custom-parser.js │ │ │ └── server.js │ │ └── support/ │ │ └── webpack.config.js │ ├── es-modules/ │ │ ├── README.md │ │ ├── client.js │ │ ├── package.json │ │ └── server.js │ ├── expo-example/ │ │ ├── .gitignore │ │ ├── App.js │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── package.json │ │ ├── server/ │ │ │ ├── index.js │ │ │ └── package.json │ │ └── socket.js │ ├── express-session-example/ │ │ ├── README.md │ │ ├── cjs/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── esm/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── package.json │ │ └── ts/ │ │ ├── index.html │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── nestjs-example/ │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── README.md │ │ ├── nest-cli.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.controller.spec.ts │ │ │ ├── app.controller.ts │ │ │ ├── app.module.ts │ │ │ ├── app.service.ts │ │ │ ├── events/ │ │ │ │ ├── events.gateway.ts │ │ │ │ └── events.module.ts │ │ │ └── main.ts │ │ ├── test/ │ │ │ ├── app.e2e-spec.ts │ │ │ └── jest-e2e.json │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── views/ │ │ └── index.hbs │ ├── nextjs-app-router/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── jsconfig.json │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── server.js │ │ └── src/ │ │ ├── app/ │ │ │ ├── globals.css │ │ │ ├── layout.js │ │ │ ├── page.js │ │ │ └── page.module.css │ │ └── socket.js │ ├── nextjs-pages-router/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── jsconfig.json │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── server.js │ │ └── src/ │ │ ├── pages/ │ │ │ ├── _app.js │ │ │ ├── _document.js │ │ │ ├── api/ │ │ │ │ └── hello.js │ │ │ └── index.js │ │ ├── socket.js │ │ └── styles/ │ │ ├── Home.module.css │ │ └── globals.css │ ├── nuxt-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.vue │ │ ├── components/ │ │ │ ├── Connection.client.vue │ │ │ └── socket.ts │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── server/ │ │ │ ├── plugins/ │ │ │ │ └── socket.io.ts │ │ │ └── tsconfig.json │ │ └── tsconfig.json │ ├── nwjs-example/ │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ └── server/ │ │ ├── index.js │ │ └── package.json │ ├── passport-example/ │ │ ├── README.md │ │ ├── cjs/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── login.html │ │ │ └── package.json │ │ ├── esm/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── login.html │ │ │ └── package.json │ │ └── ts/ │ │ ├── index.html │ │ ├── index.ts │ │ ├── login.html │ │ ├── package.json │ │ └── tsconfig.json │ ├── passport-jwt-example/ │ │ ├── README.md │ │ ├── cjs/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── esm/ │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── package.json │ │ └── ts/ │ │ ├── index.html │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── postgres-adapter-example/ │ │ ├── README.md │ │ ├── client.js │ │ ├── cluster.js │ │ ├── compose.yaml │ │ ├── package.json │ │ └── server.js │ ├── private-messaging/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ ├── server/ │ │ │ ├── cluster.js │ │ │ ├── docker-compose.yml │ │ │ ├── index.js │ │ │ ├── messageStore.js │ │ │ ├── package.json │ │ │ └── sessionStore.js │ │ └── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ ├── Chat.vue │ │ │ ├── MessagePanel.vue │ │ │ ├── SelectUsername.vue │ │ │ ├── StatusIcon.vue │ │ │ └── User.vue │ │ ├── main.js │ │ └── socket.js │ ├── rollup-server-bundle/ │ │ ├── .gitignore │ │ ├── index.js │ │ ├── package.json │ │ └── rollup.config.js │ ├── tweet-stream/ │ │ ├── index.js │ │ └── package.json │ ├── typescript-client-example/ │ │ ├── cjs/ │ │ │ ├── client.ts │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ └── esm/ │ │ ├── client.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── typescript-example/ │ │ ├── cjs/ │ │ │ ├── client.ts │ │ │ ├── package.json │ │ │ ├── server.ts │ │ │ └── tsconfig.json │ │ └── esm/ │ │ ├── client.ts │ │ ├── package.json │ │ ├── server.ts │ │ └── tsconfig.json │ ├── webpack-build/ │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── webpack-build-server/ │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── webtransport/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── generate_cert.sh │ │ ├── index.html │ │ ├── index.js │ │ ├── open_chrome.sh │ │ └── package.json │ └── whiteboard/ │ ├── README.md │ ├── index.js │ ├── package.json │ └── public/ │ ├── index.html │ ├── main.js │ └── style.css ├── package.json └── packages/ ├── engine.io/ │ ├── .eslintrc.json │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── examples/ │ │ ├── esm-import/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── latency/ │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ └── public/ │ │ │ ├── index.js │ │ │ └── style.css │ │ ├── memory-usage/ │ │ │ ├── .gitignore │ │ │ ├── client.js │ │ │ ├── package.json │ │ │ └── server.js │ │ └── memory-usage-webtransport/ │ │ ├── .gitignore │ │ ├── client.js │ │ ├── generate_cert.sh │ │ ├── package.json │ │ └── server.js │ ├── lib/ │ │ ├── contrib/ │ │ │ └── types.cookie.ts │ │ ├── engine.io.ts │ │ ├── parser-v3/ │ │ │ ├── index.ts │ │ │ └── utf8.ts │ │ ├── server.ts │ │ ├── socket.ts │ │ ├── transport.ts │ │ ├── transports/ │ │ │ ├── index.ts │ │ │ ├── polling-jsonp.ts │ │ │ ├── polling.ts │ │ │ ├── websocket.ts │ │ │ └── webtransport.ts │ │ ├── transports-uws/ │ │ │ ├── index.ts │ │ │ ├── polling.ts │ │ │ └── websocket.ts │ │ └── userver.ts │ ├── package.json │ ├── test/ │ │ ├── .eslintrc.json │ │ ├── common.js │ │ ├── engine.io.js │ │ ├── fixtures/ │ │ │ ├── ca.crt │ │ │ ├── ca.key │ │ │ ├── client.crt │ │ │ ├── client.csr │ │ │ ├── client.key │ │ │ ├── client.pfx │ │ │ ├── generate_certs.sh │ │ │ ├── server-close-upgraded.js │ │ │ ├── server-close-upgrading.js │ │ │ ├── server-close.js │ │ │ ├── server.crt │ │ │ ├── server.csr │ │ │ └── server.key │ │ ├── middlewares.js │ │ ├── parser.js │ │ ├── server.js │ │ ├── util.mjs │ │ └── webtransport.mjs │ ├── tsconfig.json │ └── wrapper.mjs ├── engine.io-client/ │ ├── .gitignore │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── lib/ │ │ ├── browser-entrypoint.ts │ │ ├── contrib/ │ │ │ ├── has-cors.ts │ │ │ ├── parseqs.ts │ │ │ └── parseuri.ts │ │ ├── globals.node.ts │ │ ├── globals.ts │ │ ├── index.ts │ │ ├── socket.ts │ │ ├── transport.ts │ │ ├── transports/ │ │ │ ├── index.ts │ │ │ ├── polling-fetch.ts │ │ │ ├── polling-xhr.node.ts │ │ │ ├── polling-xhr.ts │ │ │ ├── polling.ts │ │ │ ├── websocket.node.ts │ │ │ ├── websocket.ts │ │ │ └── webtransport.ts │ │ └── util.ts │ ├── package.json │ ├── postcompile.sh │ ├── support/ │ │ ├── bundle-size.js │ │ ├── package.cjs.json │ │ ├── package.esm.json │ │ ├── prod.config.js │ │ ├── rollup.config.esm.js │ │ ├── rollup.config.umd.js │ │ └── webpack.config.js │ ├── test/ │ │ ├── arraybuffer/ │ │ │ ├── index.js │ │ │ ├── polling.js │ │ │ └── ws.js │ │ ├── binary-fallback.js │ │ ├── blob/ │ │ │ ├── index.js │ │ │ ├── polling.js │ │ │ └── ws.js │ │ ├── connection.js │ │ ├── engine.io-client.js │ │ ├── fixtures/ │ │ │ ├── no-unref.js │ │ │ ├── unref-polling-only.js │ │ │ ├── unref-websocket-only.js │ │ │ └── unref.js │ │ ├── index.js │ │ ├── node.js │ │ ├── parseuri.js │ │ ├── socket.js │ │ ├── support/ │ │ │ ├── env.js │ │ │ ├── hooks.js │ │ │ ├── public/ │ │ │ │ └── worker.js │ │ │ └── server.js │ │ ├── transport.js │ │ ├── util-wt.mjs │ │ ├── util.js │ │ ├── webtransport.mjs │ │ └── xmlhttprequest.js │ ├── tsconfig.esm.json │ └── tsconfig.json ├── engine.io-parser/ │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── Readme.md │ ├── benchmarks/ │ │ ├── index.js │ │ └── results.md │ ├── lib/ │ │ ├── commons.ts │ │ ├── contrib/ │ │ │ └── base64-arraybuffer.ts │ │ ├── decodePacket.browser.ts │ │ ├── decodePacket.ts │ │ ├── encodePacket.browser.ts │ │ ├── encodePacket.ts │ │ └── index.ts │ ├── package.json │ ├── postcompile.sh │ ├── support/ │ │ ├── package.cjs.json │ │ └── package.esm.json │ ├── test/ │ │ ├── browser.ts │ │ ├── index.ts │ │ ├── node.ts │ │ └── util.ts │ ├── tsconfig.esm.json │ └── tsconfig.json ├── socket.io/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── RELEASING.md │ ├── Readme.md │ ├── client-dist/ │ │ └── socket.io.js │ ├── lib/ │ │ ├── broadcast-operator.ts │ │ ├── client.ts │ │ ├── index.ts │ │ ├── namespace.ts │ │ ├── parent-namespace.ts │ │ ├── socket-types.ts │ │ ├── socket.ts │ │ ├── typed-events.ts │ │ └── uws.ts │ ├── package.json │ ├── test/ │ │ ├── close.ts │ │ ├── connection-state-recovery.ts │ │ ├── fixtures/ │ │ │ ├── big.json │ │ │ └── server-close.ts │ │ ├── handshake.ts │ │ ├── index.ts │ │ ├── messaging-many.ts │ │ ├── middleware.ts │ │ ├── namespaces.ts │ │ ├── server-attachment.ts │ │ ├── socket-middleware.ts │ │ ├── socket-timeout.ts │ │ ├── socket.io.test-d.ts │ │ ├── socket.ts │ │ ├── support/ │ │ │ ├── expectjs.d.ts │ │ │ └── util.ts │ │ ├── utility-methods.ts │ │ ├── uws.ts │ │ └── v2-compatibility.ts │ ├── tsconfig.json │ └── wrapper.mjs ├── socket.io-adapter/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── Readme.md │ ├── lib/ │ │ ├── cluster-adapter.ts │ │ ├── contrib/ │ │ │ └── yeast.ts │ │ ├── in-memory-adapter.ts │ │ └── index.ts │ ├── package.json │ ├── test/ │ │ ├── cluster-adapter.ts │ │ ├── index.ts │ │ └── util.ts │ └── tsconfig.json ├── socket.io-client/ │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── RELEASING.md │ ├── babel.config.js │ ├── docs/ │ │ └── README.md │ ├── lib/ │ │ ├── browser-entrypoint.ts │ │ ├── contrib/ │ │ │ └── backo2.ts │ │ ├── index.ts │ │ ├── manager.ts │ │ ├── on.ts │ │ ├── socket.ts │ │ └── url.ts │ ├── package.json │ ├── postcompile.sh │ ├── support/ │ │ ├── bundle-size.js │ │ ├── package.esm.json │ │ ├── rollup.config.esm.js │ │ ├── rollup.config.umd.js │ │ └── rollup.config.umd.msgpack.js │ ├── test/ │ │ ├── .eslintrc.json │ │ ├── browser-runner.ts │ │ ├── connection-state-recovery.ts │ │ ├── connection.ts │ │ ├── fixtures/ │ │ │ ├── no-unref.ts │ │ │ ├── unref-during-reconnection.ts │ │ │ ├── unref-polling-only.ts │ │ │ ├── unref-websocket-only.ts │ │ │ └── unref.ts │ │ ├── index.ts │ │ ├── node.ts │ │ ├── retry.ts │ │ ├── socket.ts │ │ ├── support/ │ │ │ ├── hooks.ts │ │ │ ├── server.ts │ │ │ └── util.ts │ │ ├── typed-events.test-d.ts │ │ └── url.ts │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── wdio.conf.js ├── socket.io-cluster-adapter/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── assets/ │ │ └── adapter.excalidraw │ ├── lib/ │ │ └── index.ts │ ├── package.json │ ├── test/ │ │ ├── index.ts │ │ ├── util.ts │ │ └── worker.js │ └── tsconfig.json ├── socket.io-cluster-engine/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── compose.yaml │ ├── lib/ │ │ ├── cluster.ts │ │ ├── engine.ts │ │ ├── index.ts │ │ └── redis.ts │ ├── package.json │ ├── test/ │ │ ├── cluster.ts │ │ ├── in-memory.ts │ │ ├── redis.ts │ │ ├── util.ts │ │ └── worker.js │ └── tsconfig.json ├── socket.io-component-emitter/ │ ├── History.md │ ├── LICENSE │ ├── Readme.md │ ├── component.json │ ├── lib/ │ │ ├── cjs/ │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── package.json │ │ └── esm/ │ │ ├── index.d.ts │ │ ├── index.js │ │ └── package.json │ ├── package.json │ └── test/ │ └── emitter.js ├── socket.io-parser/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.js │ ├── bench/ │ │ ├── index.js │ │ └── results.md │ ├── lib/ │ │ ├── binary.ts │ │ ├── index.ts │ │ └── is-binary.ts │ ├── package.json │ ├── postcompile.sh │ ├── support/ │ │ ├── package.cjs.json │ │ └── package.esm.json │ ├── test/ │ │ ├── arraybuffer.js │ │ ├── blob.js │ │ ├── buffer.js │ │ ├── helpers.js │ │ ├── index.js │ │ ├── parser.js │ │ └── support/ │ │ └── env.js │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── wdio.conf.js ├── socket.io-postgres-emitter/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── assets/ │ │ └── emitter.excalidraw │ ├── compose.yaml │ ├── lib/ │ │ ├── index.ts │ │ └── typed-events.ts │ ├── package.json │ ├── test/ │ │ ├── index.ts │ │ └── util.ts │ └── tsconfig.json └── socket.io-redis-streams-emitter/ ├── CHANGELOG.md ├── LICENSE ├── README.md ├── compose.yaml ├── lib/ │ ├── adapter-types.ts │ ├── index.ts │ ├── typed-events.ts │ └── util.ts ├── package.json ├── test/ │ ├── index.ts │ └── util.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: 'to triage' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Please fill the following code example: Socket.IO server version: `x.y.z` *Server* ```js import { Server } from "socket.io"; const io = new Server(3000, {}); io.on("connection", (socket) => { console.log(`connect ${socket.id}`); socket.on("disconnect", () => { console.log(`disconnect ${socket.id}`); }); }); ``` Socket.IO client version: `x.y.z` *Client* ```js import { io } from "socket.io-client"; const socket = io("ws://localhost:3000/", {}); socket.on("connect", () => { console.log(`connect ${socket.id}`); }); socket.on("disconnect", () => { console.log("disconnect"); }); ``` **Expected behavior** A clear and concise description of what you expected to happen. **Platform:** - Device: [e.g. Samsung S8] - OS: [e.g. Android 9.2] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a Question url: https://github.com/socketio/socket.io/discussions/new?category=q-a about: Ask the community for help ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: 'enhancement' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### The kind of change this PR does introduce * [x] a bug fix * [ ] a new feature * [ ] an update to the documentation * [ ] a code change that improves performance * [ ] other ### Current behavior ### New behavior ### Other information (e.g. related issues) ================================================ FILE: .github/workflows/build-examples.yml ================================================ name: Build examples on: schedule: - cron: '0 0 * * 0' permissions: contents: read jobs: build-examples: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: example: - custom-parsers - typescript-example/cjs - typescript-example/esm - typescript-client-example/cjs - typescript-client-example/esm - webpack-build - webpack-build-server - basic-crud-application/angular-client - basic-crud-application/vue-client - nextjs-pages-router - nextjs-app-router - nuxt-example steps: - name: Checkout repository uses: actions/checkout@v6 - name: Use Node.js 20 uses: actions/setup-node@v6 with: node-version: 20 - name: Build ${{ matrix.example }} run: | cd examples/${{ matrix.example }} npm install npm run build ================================================ FILE: .github/workflows/ci-browser.yml ================================================ name: CI (browser) on: push: branches: - '**' paths: - 'packages/engine.io-parser/**' - 'packages/engine.io-client/**' - 'packages/socket.io-parser/**' - 'packages/socket.io-client/**' permissions: contents: read jobs: test-browser: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Use Node.js 20 uses: actions/setup-node@v6 with: node-version: 20 - name: Install dependencies run: npm ci - name: Compile each package run: npm run compile --workspaces --if-present - name: Run tests run: npm test --workspace=socket.io-parser --workspace=socket.io-client env: BROWSERS: 1 SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - '**' pull_request: schedule: - cron: '0 0 * * 0' permissions: contents: read jobs: test-node: runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: node-version: - 20 - 22 - 24 services: redis: image: redis:7 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 postgres: image: postgres:14 env: POSTGRES_PASSWORD: changeit options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci - name: Compile each package run: npm run compile --workspaces --if-present - name: Run tests run: npm test --workspaces - name: Run tests with uws (engine.io) run: npm run test:uws --workspace=engine.io if: ${{ matrix.node-version == '18' }} - name: Run tests with fetch instead of XHR (engine.io-client) run: npm run test:node-fetch --workspace=engine.io-client if: ${{ matrix.node-version == '18' }} - name: Run tests with Node.js native WebSocket (engine.io-client) run: npm run test:node-builtin-ws --workspace=engine.io-client if: ${{ matrix.node-version == '22' }} ================================================ FILE: .github/workflows/publish.yml ================================================ # reference: https://docs.npmjs.com/trusted-publishers#for-github-actions name: Publish on: push: tags: # expected format: @ (example: socket.io@1.2.3) - '**@*' jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - name: Checkout repository uses: actions/checkout@v6 - name: Use Node.js 24 uses: actions/setup-node@v6 with: node-version: 24 registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci - name: Compile each package run: npm run compile --workspaces --if-present - name: Publish package run: npm publish --workspace=${GITHUB_REF_NAME%@*} --access public ================================================ FILE: .gitignore ================================================ .DS_Store lib-cov *.seed *.log *.csv *.dat *.out *.pid benchmarks/*.png node_modules coverage .idea .nyc_output dist/ build/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog Here are the detailed changelogs for each package in this monorepo: | Package | Changelog | |------------------------------------|----------------------------------------------------------------| | `engine.io` | [link](packages/engine.io/CHANGELOG.md) | | `engine.io-client` | [link](packages/engine.io-client/CHANGELOG.md) | | `engine.io-parser` | [link](packages/engine.io-parser/CHANGELOG.md) | | `socket.io` | [link](packages/socket.io/CHANGELOG.md) | | `socket.io-adapter` | [link](packages/socket.io-adapter/CHANGELOG.md) | | `socket.io-client` | [link](packages/socket.io-client/CHANGELOG.md) | | `@socket.io/cluster-adapter` | [link](packages/socket.io-cluster-adapter/CHANGELOG.md) | | `@socket.io/cluster-engine` | [link](packages/socket.io-cluster-engine/CHANGELOG.md) | | `@socket.io/component-emitter` | [link](packages/socket.io-component-emitter/History.md) | | `socket.io-parser` | [link](packages/socket.io-parser/CHANGELOG.md) | | `@socket.io/postgres-emitter` | [link](packages/socket.io-postgres-emitter/CHANGELOG.md) | | `@socket.io/redis-streams-emitter` | [link](/packages/socket.io-redis-streams-emitter/CHANGELOG.md) | ================================================ FILE: CONTRIBUTING.md ================================================ # Socket.IO Contributing Guide Thanks a lot for your interest in contributing to Socket.IO! Before submitting your contribution, please make sure to take a moment and read through the following guidelines: * [Before you start](#before-you-start) * [Guidelines for reporting a bug](#guidelines-for-reporting-a-bug) * [Guidelines for requesting a feature](#guidelines-for-requesting-a-feature) * [Guidelines for creating a pull request](#guidelines-for-creating-a-pull-request) * [Bug fix](#bug-fix) * [New feature](#new-feature) * [Project structure](#project-structure) * [Development setup](#development-setup) * [Commands](#commands) * [Compile with TypeScript](#compile-with-typescript) * [Apply formatting](#apply-formatting) * [Run the tests](#run-the-tests) ## Before you start Our [issues list](https://github.com/socketio/socket.io/issues) is exclusively reserved for bug reports and feature requests. For usage questions, please use the following resources: - read the [docs](https://socket.io/docs/v4/) - check the [troubleshooting guide](https://socket.io/docs/v4/troubleshooting-connection-issues/) - look for/ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/socket.io) - create a new [discussion](https://github.com/socketio/socket.io/discussions/new?category=q-a) ## Guidelines for reporting a bug If you think that you have found a security vulnerability in our project, please do not create an issue in this GitHub repository, but rather refer to our [security policy](./SECURITY.md). Please make sure that the bug hasn't already been reported in our [issues list](https://github.com/socketio/socket.io/issues?q=label%3Abug+), as it may already have been fixed in a recent version. However, if the bug was reported in an old, closed issue but persists, you should open a new issue instead of commenting on the old issue. After these checks, please [create a new bug report](https://github.com/socketio/socket.io/issues/new/choose) with all the necessary details: - package versions - platform (device, browser, operating system) - a minimal reproduction (you can fork [this repository](https://github.com/socketio/socket.io-fiddle)) Without a clear way to reproduce the bug, we unfortunately won't be able to help you. ## Guidelines for requesting a feature Please make sure that the feature hasn't already been requested in our [issues list](https://github.com/socketio/socket.io/labels/enhancement). After these checks, please [create a new feature request](https://github.com/socketio/socket.io/issues/new/choose) with all the necessary details: - what the problem is - what you want to happen - any alternative solutions or features you have considered ## Guidelines for creating a pull request ### Bug fix - if you fix a bug which is described in our [issues list](https://github.com/socketio/socket.io/issues), please add a reference to it in the description of your pull request. Otherwise, please provide all necessary details to reproduce the bug, as described [above](#guidelines-for-reporting-a-bug). - add one or more test cases, in order to avoid any regression in the future - make sure existing tests still pass ### New feature - we strongly suggest that you first open a [feature request](#guidelines-for-requesting-a-feature) and have it approved before working on it. In that case, please add a reference to it in the description of your pull request. - add one or more test cases, in order to avoid any regression in the future - make sure existing tests still pass ## Project structure This repository is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) which contains the source of the following packages: | Package | Description | |--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| | `engine.io` | The server-side implementation of the low-level communication layer. | | `engine.io-client` | The client-side implementation of the low-level communication layer. | | `engine.io-parser` | The parser responsible for encoding and decoding Engine.IO packets, used by both the `engine.io` and `engine.io-client` packages. | | `socket.io` | The server-side implementation of the bidirectional channel, built on top on the `engine.io` package. | | `socket.io-adapter` | An extensible component responsible for broadcasting a packet to all connected clients, used by the `socket.io` package. | | `socket.io-client` | The client-side implementation of the bidirectional channel, built on top on the `engine.io-client` package. | | `@socket.io/cluster-engine` | A cluster-friendly engine to share load between multiple Node.js processes (without sticky sessions) | | `@socket.io/component-emitter` | An `EventEmitter` implementation, similar to the one provided by [Node.js](https://nodejs.org/api/events.html) but for all platforms. | | `socket.io-parser` | The parser responsible for encoding and decoding Socket.IO packets, used by both the `socket.io` and `socket.io-client` packages. | ## Development setup You will need [Node.js](https://nodejs.org) **version 18+**, and [`npm`](https://docs.npmjs.com/about-npm) **version 7+**, as we make use of npm's [workspaces feature](https://docs.npmjs.com/cli/v10/using-npm/workspaces). After cloning the repository, please run: ```bash npm ci ``` to install all dependencies. Here is the list of tools that we use: - [TypeScript](https://www.typescriptlang.org/) as the development language - [Rollup](https://rollupjs.org/) for production bundling - [Prettier](https://prettier.io/) for code formatting - [Mocha](https://mochajs.org/) for testing - [WebdriverIO](https://webdriver.io/) for browser and mobile testing ## Commands Each npm workspace corresponds to a package. You can run the command: - on all workspaces with the `--workspace` command-line argument (abbreviated `-ws`) - on a specific workspace with the `--workspace=` command-line argument ### Compile with TypeScript For all workspaces: ```bash npm run compile -ws --if-present ``` For a specific workspace: ```bash npm run compile --workspace=socket.io ``` ### Apply formatting For all workspaces: ```bash npm run format:fix -ws ``` For a specific workspace: ```bash npm run format:fix --workspace=socket.io ``` ### Run the tests For all workspaces: ```bash npm test -ws ``` For a specific workspace: ```bash npm test --workspace=socket.io ``` ### Generate the changelog Install the [`conventional-changelog-cli`](https://www.npmjs.com/package/conventional-changelog-cli) package: ```bash npm i -g conventional-changelog-cli ``` Then run: ```bash cd packages/engine.io-client conventional-changelog -p angular --tag-prefix "engine.io-client@" --commit-path . ``` ================================================ FILE: LICENSE ================================================ (The MIT License) Copyright (c) 2014-present Guillermo Rauch and Socket.IO contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # socket.io [![Latest NPM version](https://img.shields.io/npm/v/socket.io.svg)](https://www.npmjs.com/package/socket.io) [![Build status](https://github.com/socketio/socket.io/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/socketio/socket.io/actions/workflows/ci.yml) [![Downloads per month](https://img.shields.io/npm/dm/socket.io.svg)]((https://www.npmjs.com/package/socket.io)) ## Getting Started Please check our documentation [here](https://socket.io). ## Questions Our [issues list](https://github.com/socketio/socket.io/issues) is exclusively reserved for bug reports and feature requests. For usage questions, please use the following resources: - read our [documentation](https://socket.io/docs/v4/) - check our [troubleshooting guide](https://socket.io/docs/v4/troubleshooting-connection-issues/) - look for/ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/socket.io) - create a new [discussion](https://github.com/socketio/socket.io/discussions/new?category=q-a) ## Security If you think that you have found a security vulnerability in our project, please do not create an issue in this GitHub repository, but rather refer to our [Security Policy](./SECURITY.md). ## Issues and contribution Please make sure to read our [Contributing Guide](./CONTRIBUTING.md) before creating an issue or making a pull request. Thanks to everyone who has already contributed to Socket.IO! ## License [MIT](https://opensource.org/licenses/MIT) ================================================ FILE: SECURITY.md ================================================ # Security Policy * [Supported Versions](#supported-versions) * [Reporting a Vulnerability](#reporting-a-vulnerability) * [History](#history) * [For the `socket.io` package](#for-the-socketio-package) * [For the `socket.io-client` package](#for-the-socketio-client-package) ## Supported Versions | Version | Supported | |---------|--------------------| | 4.x | :white_check_mark: | | 3.x | :white_check_mark: | | 2.4.x | :white_check_mark: | | < 2.4.0 | :x: | ## Reporting a Vulnerability To report a security vulnerability in this package, please send an email to [@darrachequesne](https://github.com/darrachequesne) (see address in profile) describing the vulnerability and how to reproduce it. We will get back to you as soon as possible and publish a fix if necessary. :warning: IMPORTANT :warning: please do not create an issue in this repository, as attackers might take advantage of it. Thank you in advance for your responsible disclosure. ## History ### For the `socket.io` package | Date | Description | CVE number | Affected versions | Patched versions | |--------------|------------------------------------------------------------------------------|------------------|-------------------------------------|-----------------------| | July 2012 | [Insecure randomness](https://github.com/advisories/GHSA-qv2v-m59f-v5fw) | `CVE-2017-16031` | `<= 0.9.6` | `0.9.7` | | January 2021 | [CORS misconfiguration](https://github.com/advisories/GHSA-fxwf-4rqh-v8g3) | `CVE-2020-28481` | `< 2.4.0` | `2.4.0` | | June 2024 | [Unhandled 'error' event](https://github.com/advisories/GHSA-25hc-qcg6-38wj) | `CVE-2024-38355` | `< 2.5.1`
`>= 3.0.0, < 4.6.2` | `2.5.1`
`4.6.2` | From the transitive dependencies: | Date | Dependency | Description | CVE number | |---------------|--------------------|-------------------------------------------------------------------------------------------------------------------------|------------------| | January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` | | January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` | | November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` | | February 2020 | `engine.io` | [Resource exhaustion](https://github.com/advisories/GHSA-j4f2-536g-r55m) | `CVE-2020-36048` | | January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` | | May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` | | January 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-273r-mgr4-v34f) | `CVE-2022-21676` | | October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` | | November 2022 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-r7qp-cfhv-p84w) | `CVE-2022-41940` | | May 2023 | `engine.io` | [Uncaught exception](https://github.com/advisories/GHSA-q9mw-68c2-j6m5) | `CVE-2023-31125` | | May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` | | June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` | | March 2026 | `socket.io-parser` | [Unbounded number of binary attachments](https://github.com/socketio/socket.io/security/advisories/GHSA-677m-j7p3-52f9) | `CVE-2026-33151` | ### For the `socket.io-client` package From the transitive dependencies: | Date | Dependency | Description | CVE number | |---------------|--------------------|-------------------------------------------------------------------------------------------------------------------------|------------------| | January 2016 | `ws` | [Buffer vulnerability](https://github.com/advisories/GHSA-2mhh-w6q8-5hxw) | `CVE-2016-10518` | | January 2016 | `ws` | [DoS due to excessively large websocket message](https://github.com/advisories/GHSA-6663-c963-2gqg) | `CVE-2016-10542` | | October 2016 | `engine.io-client` | [Insecure Defaults Allow MITM Over TLS](https://github.com/advisories/GHSA-4r4m-hjwj-43p8) | `CVE-2016-10536` | | November 2017 | `ws` | [DoS in the `Sec-Websocket-Extensions` header parser](https://github.com/advisories/GHSA-5v72-xg48-5rpm) | `-` | | January 2021 | `socket.io-parser` | [Resource exhaustion](https://github.com/advisories/GHSA-xfhh-g9f5-x4m4) | `CVE-2020-36049` | | May 2021 | `ws` | [ReDoS in `Sec-Websocket-Protocol` header](https://github.com/advisories/GHSA-6fc8-4gx4-v693) | `CVE-2021-32640` | | October 2022 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-qm95-pgcg-qqfq) | `CVE-2022-2421` | | May 2023 | `socket.io-parser` | [Insufficient validation when decoding a Socket.IO packet](https://github.com/advisories/GHSA-cqmj-92xf-r6r9) | `CVE-2023-32695` | | June 2024 | `ws` | [DoS when handling a request with many HTTP headers](https://github.com/advisories/GHSA-3h5v-q93c-6h6q) | `CVE-2024-37890` | | March 2026 | `socket.io-parser` | [Unbounded number of binary attachments](https://github.com/socketio/socket.io/security/advisories/GHSA-677m-j7p3-52f9) | `CVE-2026-33151` | ================================================ FILE: docs/README.md ================================================ The documentation has been moved to the website [here](https://socket.io/docs/). ================================================ FILE: docs/engine.io-protocol/v3-test-suite/.gitignore ================================================ node_modules ================================================ FILE: docs/engine.io-protocol/v3-test-suite/index.html ================================================ Test suite for the Engine.IO protocol
================================================ FILE: docs/engine.io-protocol/v3-test-suite/node-imports.js ================================================ import fetch from "node-fetch"; import { WebSocket } from "ws"; import chai from "chai"; import chaiString from "chai-string"; chai.use(chaiString); globalThis.fetch = fetch; globalThis.WebSocket = WebSocket; globalThis.chai = chai; ================================================ FILE: docs/engine.io-protocol/v3-test-suite/package.json ================================================ { "name": "engine.io-protocol-test-suite", "version": "0.0.1", "private": true, "type": "module", "scripts": { "format": "prettier -w *.js", "test": "mocha test-suite.js" }, "devDependencies": { "chai": "^4.3.6", "chai-string": "^1.5.0", "mocha": "^9.2.1", "node-fetch": "^3.2.0", "prettier": "^2.5.1", "ws": "^8.5.0" } } ================================================ FILE: docs/engine.io-protocol/v3-test-suite/test-suite.js ================================================ const isNodejs = typeof window === "undefined"; if (isNodejs) { // make the tests runnable in both the browser and Node.js await import("./node-imports.js"); } const { expect } = chai; const URL = "http://localhost:3000"; const WS_URL = URL.replace("http", "ws"); const PING_INTERVAL = 300; const PING_TIMEOUT = 200; function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } function waitFor(socket, eventType) { return new Promise((resolve) => { socket.addEventListener( eventType, (event) => { resolve(event); }, { once: true } ); }); } function decodePayload(payload) { const firstColonIndex = payload.indexOf(":"); const length = payload.substring(0, firstColonIndex); const packet = payload.substring(firstColonIndex + 1); return [length, packet]; } async function initLongPollingSession(supportsBinary = false) { const response = await fetch(`${URL}/engine.io/?EIO=3&transport=polling` + (supportsBinary ? "" : "&b64=1")); const text = await response.text(); const [, content] = decodePayload(text); return JSON.parse(content.substring(1)).sid; } describe("Engine.IO protocol", () => { describe("handshake", () => { describe("HTTP long-polling", () => { it("successfully opens a session", async () => { const response = await fetch( `${URL}/engine.io/?EIO=3&transport=polling` ); expect(response.status).to.eql(200); const text = await response.text(); const [length, content] = decodePayload(text); expect(length).to.eql(content.length.toString()); expect(content).to.startsWith("0"); const value = JSON.parse(content.substring(1)); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql(["websocket"]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.be.oneOf([undefined, 1000000]); }); it("fails with an invalid 'transport' query parameter", async () => { const response = await fetch(`${URL}/engine.io/?EIO=3`); expect(response.status).to.eql(400); const response2 = await fetch(`${URL}/engine.io/?EIO=3&transport=abc`); expect(response2.status).to.eql(400); }); it("fails with an invalid request method", async () => { const response = await fetch( `${URL}/engine.io/?EIO=3&transport=polling`, { method: "post", } ); expect(response.status).to.eql(400); }); }); describe("WebSocket", () => { it("successfully opens a session", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("0"); const value = JSON.parse(data.substring(1)); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql([]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.be.oneOf([undefined, 1000000]); socket.close(); }); it("fails with an invalid 'EIO' query parameter", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?transport=websocket` ); if (isNodejs) { socket.on("error", () => {}); } waitFor(socket, "close"); const socket2 = new WebSocket( `${WS_URL}/engine.io/?EIO=abc&transport=websocket` ); if (isNodejs) { socket2.on("error", () => {}); } waitFor(socket2, "close"); }); it("fails with an invalid 'transport' query parameter", async () => { const socket = new WebSocket(`${WS_URL}/engine.io/?EIO=3`); if (isNodejs) { socket.on("error", () => {}); } waitFor(socket, "close"); const socket2 = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=abc` ); if (isNodejs) { socket2.on("error", () => {}); } waitFor(socket2, "close"); }); }); }); describe("message", () => { describe("HTTP long-polling", () => { it("sends and receives a payload containing one plain text packet", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "6:4hello", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("6:4hello"); }); it("sends and receives a payload containing several plain text packets", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "6:4test16:4test26:4test3", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("6:4test16:4test26:4test3"); }); it("sends and receives a payload containing plain text and binary packets (base64 encoded)", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "6:4hello10:b4AQIDBA==", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("6:4hello10:b4AQIDBA=="); }); it("sends and receives a payload containing plain text and binary packets (binary)", async () => { const sid = await initLongPollingSession(true); const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "6:4hello10:b4AQIDBA==", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const buffer = await pollResponse.arrayBuffer(); // 0 => string // 6 => byte length // 255 => delimiter // 52 => 4 (MESSAGE packet type) // 104 101 108 108 111 => "hello" // 1 => binary // 5 => byte length // 255 => delimiter // 4 => 4 (MESSAGE packet type) // 1 2 3 4 => binary message expect(buffer).to.eql(Uint8Array.from([0, 6, 255, 52, 104, 101, 108, 108, 111, 1, 5, 255, 4, 1, 2, 3, 4]).buffer); }); it("closes the session upon invalid packet format", async () => { const sid = await initLongPollingSession(); try { const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "abc", } ); expect(pushResponse.status).to.eql(400); } catch (e) { // node-fetch throws when the request is closed abnormally } const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); // FIXME CORS error it.skip("closes the session upon duplicate poll requests", async () => { const sid = await initLongPollingSession(); const pollResponses = await Promise.all([ fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`), sleep(5).then(() => fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}&t=burst`)), ]); expect(pollResponses[0].status).to.eql(200); const content = await pollResponses[0].text(); expect(content).to.eql("1:1"); // the Node.js implementation uses HTTP 500 (Internal Server Error), but HTTP 400 seems more suitable expect(pollResponses[1].status).to.be.oneOf([400, 500]); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(500); }); }); describe("WebSocket", () => { it("sends and receives a plain text packet", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); await waitFor(socket, "open"); await waitFor(socket, "message"); // handshake socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); socket.close(); }); it("sends and receives a binary packet", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); socket.binaryType = "arraybuffer"; await waitFor(socket, "message"); // handshake socket.send(Uint8Array.from([4, 1, 2, 3, 4])); const { data } = await waitFor(socket, "message"); expect(data).to.eql(Uint8Array.from([4, 1, 2, 3, 4]).buffer); socket.close(); }); it("closes the session upon invalid packet format", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // handshake socket.send("abc"); await waitFor(socket, "close"); socket.close(); }); }); }); describe("heartbeat", function () { this.timeout(5000); describe("HTTP long-polling", () => { it("sends ping/pong packets", async () => { const sid = await initLongPollingSession(); for (let i = 0; i < 3; i++) { const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "1:2", } ); expect(pushResponse.status).to.eql(200); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("1:3"); } }); it("closes the session upon ping timeout", async () => { const sid = await initLongPollingSession(); await sleep(PING_INTERVAL + PING_TIMEOUT); const pushResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "1:2", } ); expect(pushResponse.status).to.eql(400); }); }); describe("WebSocket", () => { it("sends ping/pong packets", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); const x = await waitFor(socket, "message"); // handshake for (let i = 0; i < 3; i++) { socket.send("2"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("3"); } socket.close(); }); it("closes the session upon ping timeout", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); await waitFor(socket, "close"); // handshake }); }); }); describe("close", () => { describe("HTTP long-polling", () => { it("forcefully closes the session", async () => { const sid = await initLongPollingSession(); const [pollResponse] = await Promise.all([ fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`), fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, { method: "post", body: "1:1", }), ]); expect(pollResponse.status).to.eql(200); const pullContent = await pollResponse.text(); expect(pullContent).to.eql("1:6"); const pollResponse2 = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse2.status).to.eql(400); }); }); describe("WebSocket", () => { it("forcefully closes the session", async () => { const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket` ); await waitFor(socket, "message"); // handshake socket.send("1"); await waitFor(socket, "close"); }); }); }); describe("upgrade", () => { it("successfully upgrades from HTTP long-polling to WebSocket", async () => { const sid = await initLongPollingSession(); const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); // send probe socket.send("2probe"); const probeResponse = await waitFor(socket, "message"); expect(probeResponse.data).to.eql("3probe"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("1:6"); // "noop" packet to cleanly end the HTTP long-polling request // complete upgrade socket.send("5"); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); it("ignores HTTP requests with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); const res = await waitFor(socket, "message"); expect(res.data).to.eql("3probe"); socket.send("5"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); it("ignores WebSocket connection with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); const res = await waitFor(socket, "message"); expect(res.data).to.eql("3probe"); socket.send("5"); const socket2 = new WebSocket( `${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}` ); await waitFor(socket2, "close"); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); }); }); ================================================ FILE: docs/engine.io-protocol/v3.md ================================================ # Engine.IO Protocol This document describes the Engine.IO protocol. For a reference JavaScript implementation, take a look at [engine.io-parser](https://github.com/learnboost/engine.io-parser), [engine.io-client](https://github.com/learnboost/engine.io-client) and [engine.io](https://github.com/learnboost/engine.io). Table of Contents: - [Revision](#revision) - [Anatomy of an Engine.IO session](#anatomy-of-an-engineio-session) - [Sample session](#sample-session) - [Sample session with WebSocket only](#sample-session-with-websocket-only) - [URLs](#urls) - [Encoding](#encoding) - [Packet](#packet) - [0 open](#0-open) - [1 close](#1-close) - [2 ping](#2-ping) - [3 pong](#3-pong) - [4 message](#4-message) - [5 upgrade](#5-upgrade) - [6 noop](#6-noop) - [Payload](#payload) - [Transports](#transports) - [Polling](#polling) - [XHR](#xhr) - [JSONP](#jsonp) - [WebSocket](#websocket) - [Transport upgrading](#transport-upgrading) - [Timeouts](#timeouts) - [Difference between v2 and v3](#difference-between-v2-and-v3) - [Test suite](#test-suite) ## Revision This is revision **3** of the Engine.IO protocol. The revision 2 can be found here: https://github.com/socketio/engine.io-protocol/tree/v2 ## Anatomy of an Engine.IO session 1. Transport establishes a connection to the Engine.IO URL . 2. Server responds with an `open` packet with JSON-encoded handshake data: - `sid` session id (`String`) - `upgrades` possible transport upgrades (`Array` of `String`) - `pingTimeout` server configured ping timeout, used for the client to detect that the server is unresponsive (`Number`) - `pingInterval` server configured ping interval, used for the client to detect that the server is unresponsive (`Number`) 3. Server must respond to periodic `ping` packets sent by the client with `pong` packets. 4. Client and server can exchange `message` packets at will. 5. Polling transports can send a `close` packet to close the socket, since they're expected to be "opening" and "closing" all the time. ### Sample session - Request n°1 (open packet) ``` GET /engine.io/?EIO=3&transport=polling&t=N8hyd6w < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 96:0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000} ``` Details: ``` 96 => number of characters (not bytes) : => separator 0 => "open" packet type {"sid":... => the handshake data ``` Note: the `t` query param is used to ensure that the request is not cached by the browser. - Request n°2 (message in): `socket.send('hey')` is executed on the server: ``` GET /engine.io/?EIO=3&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 4:4hey ``` Details: ``` 4 => number of characters : => separator 4 => "message" packet type hey => the actual message ``` - Request n°3 (message out) `socket.send('hello'); socket.send('world');` is executed on the client: ``` POST /engine.io/?EIO=3&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC > Content-Type: text/plain; charset=UTF-8 6:4hello6:4world < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 ok ``` Details: ``` 6 => number of characters of the 1st packet : => separator 4 => "message" packet type hello => the 1st message 6 => number of characters of the 2nd packet : => separator 4 => "message" message type world => the 2nd message ``` - Request n°4 (WebSocket upgrade) ``` GET /engine.io/?EIO=3&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 101 Switching Protocols ``` WebSocket frames: ``` < 2probe => probe request > 3probe => probe response > 5 => "upgrade" packet type > 4hello => message (not concatenated) > 4world > 2 => "ping" packet type < 3 => "pong" packet type > 1 => "close" packet type ``` ### Sample session with WebSocket only In that case, the client only enables WebSocket (without HTTP polling). ``` GET /engine.io/?EIO=3&transport=websocket < HTTP/1.1 101 Switching Protocols ``` WebSocket frames: ``` < 0{"sid":"lv_VI97HAXpY6yYWAAAC","pingInterval":25000,"pingTimeout":5000} => handshake < 4hey > 4hello => message (not concatenated) > 4world < 2 => "ping" packet type > 3 => "pong" packet type > 1 => "close" packet type ``` ## URLs An Engine.IO url is composed as follows: ``` /engine.io/[?] ``` - The `engine.io` pathname should only be changed by higher-level frameworks whose protocol sits on top of engine's. - The query string is optional and has six reserved keys: - `transport`: indicates the transport name. Supported ones by default are `polling`, `websocket`. - `j`: if the transport is `polling` but a JSONP response is required, `j` must be set with the JSONP response index. - `sid`: if the client has been given a session id, it must be included in the querystring. - `b64`: if the client doesn't support XHR2, `b64=1` is sent in the query string to signal the server that all binary data should be sent base64 encoded. - `EIO`: the version of the protocol - `t`: a hashed-timestamp used for cache-busting *FAQ:* Is the `/engine.io` portion modifiable? Provided the server is customized to intercept requests under a different path segment, yes. *FAQ:* What determines whether an option is going to be part of the path versus being encoded as part of the query string? In other words, why is the `transport` not part of the URL? It's convention that the path segments remain *only* that which allows to disambiguate whether a request should be handled by a given Engine.IO server instance or not. As it stands, it's only the Engine.IO prefix (`/engine.io`) and the resource (`default` by default). ## Encoding There's two distinct types of encodings - packet - payload ### Packet An encoded packet can be UTF-8 string or binary data. The packet encoding format for a string is as follows ``` [] ``` example: ``` 2probe ``` For binary data the encoding is identical. When sending binary data, the packet type id is sent in the first byte of the binary contents, followed by the actual packet data. Example: ``` 4|0|1|2|3|4|5 ``` In the above example each byte is separated by a pipe character and shown as an integer. So the above packet is of type message (see below), and contains binary data that corresponds to an array of integers with values 0, 1, 2, 3, 4 and 5. The packet type id is an integer. The following are the accepted packet types. #### 0 open Sent from the server when a new transport is opened (recheck) #### 1 close Request the close of this transport but does not shutdown the connection itself. #### 2 ping Sent by the client. Server should answer with a pong packet containing the same data example 1. client sends: ```2probe``` 2. server sends: ```3probe``` #### 3 pong Sent by the server to respond to ping packets. #### 4 message actual message, client and server should call their callbacks with the data. ##### example 1 1. server sends: ```4HelloWorld``` 2. client receives and calls callback ```socket.on('message', function (data) { console.log(data); });``` ##### example 2 1. client sends: ```4HelloWorld``` 2. server receives and calls callback ```socket.on('message', function (data) { console.log(data); });``` #### 5 upgrade Before engine.io switches a transport, it tests, if server and client can communicate over this transport. If this test succeed, the client sends an upgrade packets which requests the server to flush its cache on the old transport and switch to the new transport. #### 6 noop A noop packet. Used primarily to force a poll cycle when an incoming websocket connection is received. ##### example 1. client connects through new transport 2. client sends ```2probe``` 3. server receives and sends ```3probe``` 4. client receives and sends ```5``` 5. server flushes and closes old transport and switches to new. ### Payload A payload is a series of encoded packets tied together. The payload encoding format is as follows when only strings are sent and XHR2 is not supported: ``` :[:[...]] ``` * length: length of the packet in __characters__ * packet: actual packets as descriped above When XHR2 is not supported, the same encoding principle is used also when binary data is sent, but it is sent as base64 encoded strings. For the purposes of decoding, an identifier `b` is put before a packet encoding that contains binary data. A combination of any number of strings and base64 encoded strings can be sent. Here is an example of base 64 encoded messages: ``` :b[...] ``` When XHR2 is supported, a similar principle is used, but everything is encoded directly into binary, so that it can be sent as binary over XHR. The format is the following: ``` <0 for string data, 1 for binary data>[...] ``` If a combination of UTF-8 strings and binary data is sent, the string values are represented so that each character is written as a character code into a byte. The payload is used for transports which do not support framing, as the polling protocol for example. - Example without binary: ``` [ { "type": "message", "data": "hello" }, { "type": "message", "data": "€" } ] ``` is encoded to: ``` 6:4hello2:4€ ``` Please note that we are not counting bytes, but characters, hence 2 (1 + 1) instead of 4 (1 + 3). - Example with binary (both the client and the transport support binary): ``` [ { "type": "message", "data": "€" }, { "type": "message", "data": buffer <01 02 03 04> } ] ``` is encoded to: ``` buffer <00 04 ff 34 e2 82 ac 01 04 ff 01 02 03 04> with: 00 => string header 04 => string length in bytes ff => separator 34 => "message" packet type ("4") e2 82 ac => "€" 01 => binary header 04 => buffer length in bytes ff => separator 01 02 03 04 => buffer content ``` - Example with binary (either the client or the transport does not support binary): ``` [ { "type": "message", "data": "€" }, { "type": "message", "data": buffer <01 02 03 04> } ] ``` is encoded to: ``` 2:4€10:b4AQIDBA== with 2 => number of characters of the 1st packet : => separator 4 => "message" packet type € 10 => number of characters of the 2nd packet : => separator b => indicates a base64 packet 4 => "message" packet type AQIDBA== => buffer content encoded in base64 ``` ## Transports An engine.io server must support three transports: - websocket - polling - jsonp - xhr ### Polling The polling transport consists of recurring GET requests by the client to the server to get data, and POST requests with payloads from the client to the server to send data. #### XHR The server must support CORS responses. #### JSONP The server implementation must respond with valid JavaScript. The URL contains a query string parameter `j` that must be used in the response. `j` is an integer. The format of a JSONP packet. ``` `___eio[` `]("` `");` ``` To ensure that the payload gets processed correctly, it must be escaped in such a way that the response is still valid JavaScript. Passing the encoded payload through a JSON encoder is a good way to escape it. Example JSONP frame returned by the server: ``` ___eio[4]("packet data"); ``` ##### Posting data The client posts data through a hidden iframe. The data gets to the server in the URI encoded format as follows: ``` d= ``` In addition to the regular qs escaping, in order to prevent inconsistencies with `\n` handling by browsers, `\n` gets escaped as `\\n` prior to being POSTd. ### WebSocket Encoding payloads _should not_ be used for WebSocket, as the protocol already has a lightweight framing mechanism. In order to send a payload of messages, encode packets individually and `send()` them in succession. ## Transport upgrading A connection always starts with polling (either XHR or JSONP). WebSocket gets tested on the side by sending a probe. If the probe is responded from the server, an upgrade packet is sent. To ensure no messages are lost, the upgrade packet will only be sent once all the buffers of the existing transport are flushed and the transport is considered _paused_. When the server receives the upgrade packet, it must assume this is the new transport channel and send all existing buffers (if any) to it. The probe sent by the client is a `ping` packet with `probe` sent as data. The probe sent by the server is a `pong` packet with `probe` sent as data. Moving forward, upgrades other than just `polling -> x` are being considered. ## Timeouts The client must use the `pingTimeout` and the `pingInterval` sent as part of the handshake (with the `open` packet) to determine whether the server is unresponsive. The client sends a `ping` packet. If no packet type is received within `pingTimeout`, the client considers the socket disconnected. If a `pong` packet is actually received, the client will wait `pingInterval` before sending a `ping` packet again. Since the two values are shared between the server and the client, the server will also be able to detect whether the client becomes unresponsive when it does not receive any data within `pingTimeout + pingInterval`. ## Difference between v2 and v3 - add support for binary data v2 is included in Socket.IO v0.9, while v3 is included in Socket.IO v1/v2. ## Test suite The test suite in the `test-suite/` directory lets you check the compliance of a server implementation. Usage: - in Node.js: `npm ci && npm test` - in a browser: simply open the `index.html` file in your browser For reference, here is expected configuration for the JavaScript server to pass all tests: ```js import { listen } from "engine.io"; const server = listen(3000, { pingInterval: 300, pingTimeout: 200, allowEIO3: true, maxPayload: 1e6, cors: { origin: "*" } }); server.on("connection", socket => { socket.on("data", (...args) => { socket.send(...args); }); }); ``` ================================================ FILE: docs/engine.io-protocol/v4-current.md ================================================ # Engine.IO Protocol This document describes the 4th version of the Engine.IO protocol. **Table of content** - [Introduction](#introduction) - [Transports](#transports) - [HTTP long-polling](#http-long-polling) - [Request path](#request-path) - [Query parameters](#query-parameters) - [Headers](#headers) - [Sending and receiving data](#sending-and-receiving-data) - [Sending data](#sending-data) - [Receiving data](#receiving-data) - [WebSocket](#websocket) - [Protocol](#protocol) - [Handshake](#handshake) - [Heartbeat](#heartbeat) - [Upgrade](#upgrade) - [Message](#message) - [Packet encoding](#packet-encoding) - [HTTP long-polling](#http-long-polling-1) - [WebSocket](#websocket-1) - [History](#history) - [From v2 to v3](#from-v2-to-v3) - [From v3 to v4](#from-v3-to-v4) - [Test suite](#test-suite) ## Introduction The Engine.IO protocol enables [full-duplex](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX) and low-overhead communication between a client and a server. It is based on the [WebSocket protocol](https://en.wikipedia.org/wiki/WebSocket) and uses [HTTP long-polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) as fallback if the WebSocket connection can't be established. The reference implementation is written in [TypeScript](https://www.typescriptlang.org/): - server: https://github.com/socketio/engine.io - client: https://github.com/socketio/engine.io-client The [Socket.IO protocol](https://github.com/socketio/socket.io-protocol) is built on top of these foundations, bringing additional features over the communication channel provided by the Engine.IO protocol. ## Transports The connection between an Engine.IO client and an Engine.IO server can be established with: - [HTTP long-polling](#http-long-polling) - [WebSocket](#websocket) ### HTTP long-polling The HTTP long-polling transport (also simply referred as "polling") consists of successive HTTP requests: - long-running `GET` requests, for receiving data from the server - short-running `POST` requests, for sending data to the server #### Request path The path of the HTTP requests is `/engine.io/` by default. It might be updated by libraries built on top of the protocol (for example, the Socket.IO protocol uses `/socket.io/`). #### Query parameters The following query parameters are used: | Name | Value | Description | |-------------|-----------|--------------------------------------------------------------------| | `EIO` | `4` | Mandatory, the version of the protocol. | | `transport` | `polling` | Mandatory, the name of the transport. | | `sid` | `` | Mandatory once the session is established, the session identifier. | If a mandatory query parameter is missing, then the server MUST respond with an HTTP 400 error status. #### Headers When sending binary data, the sender (client or server) MUST include a `Content-Type: application/octet-stream` header. Without an explicit `Content-Type` header, the receiver SHOULD infer that the data is plaintext. Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type #### Sending and receiving data ##### Sending data To send some packets, a client MUST create an HTTP `POST` request with the packets encoded in the request body: ``` CLIENT SERVER │ │ │ POST /engine.io/?EIO=4&transport=polling&sid=... │ │ ───────────────────────────────────────────────────► │ │ ◄──────────────────────────────────────────────────┘ │ │ HTTP 200 │ │ │ ``` The server MUST return an HTTP 400 response if the session ID (from the `sid` query parameter) is not known. To indicate success, the server MUST return an HTTP 200 response, with the string `ok` in the response body. To ensure packet ordering, a client MUST NOT have more than one active `POST` request. Should it happen, the server MUST return an HTTP 400 error status and close the session. ##### Receiving data To receive some packets, a client MUST create an HTTP `GET` request: ``` CLIENT SERVER │ GET /engine.io/?EIO=4&transport=polling&sid=... │ │ ──────────────────────────────────────────────────► │ │ . │ │ . │ │ . │ │ . │ │ ◄─────────────────────────────────────────────────┘ │ │ HTTP 200 │ ``` The server MUST return an HTTP 400 response if the session ID (from the `sid` query parameter) is not known. The server MAY not respond right away if there are no packets buffered for the given session. Once there are some packets to be sent, the server SHOULD encode them (see [Packet encoding](#packet-encoding)) and send them in the response body of the HTTP request. To ensure packet ordering, a client MUST NOT have more than one active `GET` request. Should it happen, the server MUST return an HTTP 400 error status and close the session. ### WebSocket The WebSocket transport consists of a [WebSocket connection](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), which provides a bidirectional and low-latency communication channel between the server and the client. The following query parameters are used: | Name | Value | Description | |-------------|-------------|-------------------------------------------------------------------------------| | `EIO` | `4` | Mandatory, the version of the protocol. | | `transport` | `websocket` | Mandatory, the name of the transport. | | `sid` | `` | Optional, depending on whether it's an upgrade from HTTP long-polling or not. | If a mandatory query parameter is missing, then the server MUST close the WebSocket connection. Each packet (read or write) is sent its own [WebSocket frame](https://datatracker.ietf.org/doc/html/rfc6455#section-5). A client MUST NOT open more than one WebSocket connection per session. Should it happen, the server MUST close the WebSocket connection. ## Protocol An Engine.IO packet consists of: - a packet type - an optional packet payload Here is the list of available packet types: | Type | ID | Usage | |---------|-----|--------------------------------------------------| | open | 0 | Used during the [handshake](#handshake). | | close | 1 | Used to indicate that a transport can be closed. | | ping | 2 | Used in the [heartbeat mechanism](#heartbeat). | | pong | 3 | Used in the [heartbeat mechanism](#heartbeat). | | message | 4 | Used to send a payload to the other side. | | upgrade | 5 | Used during the [upgrade process](#upgrade). | | noop | 6 | Used during the [upgrade process](#upgrade). | ### Handshake To establish a connection, the client MUST send an HTTP `GET` request to the server: - HTTP long-polling first (by default) ``` CLIENT SERVER │ │ │ GET /engine.io/?EIO=4&transport=polling │ │ ───────────────────────────────────────────────────────► │ │ ◄──────────────────────────────────────────────────────┘ │ │ HTTP 200 │ │ │ ``` - WebSocket-only session ``` CLIENT SERVER │ │ │ GET /engine.io/?EIO=4&transport=websocket │ │ ───────────────────────────────────────────────────────► │ │ ◄──────────────────────────────────────────────────────┘ │ │ HTTP 101 │ │ │ ``` If the server accepts the connection, then it MUST respond with an `open` packet with the following JSON-encoded payload: | Key | Type | Description | |----------------|------------|-------------------------------------------------------------------------------------------------------------------| | `sid` | `string` | The session ID. | | `upgrades` | `string[]` | The list of available [transport upgrades](#upgrade). | | `pingInterval` | `number` | The ping interval, used in the [heartbeat mechanism](#heartbeat) (in milliseconds). | | `pingTimeout` | `number` | The ping timeout, used in the [heartbeat mechanism](#heartbeat) (in milliseconds). | | `maxPayload` | `number` | The maximum number of bytes per chunk, used by the client to aggregate packets into [payloads](#packet-encoding). | Example: ```json { "sid": "lv_VI97HAXpY6yYWAAAC", "upgrades": ["websocket"], "pingInterval": 25000, "pingTimeout": 20000, "maxPayload": 1000000 } ``` The client MUST send the `sid` value in the query parameters of all subsequent requests. ### Heartbeat Once the [handshake](#handshake) is completed, a heartbeat mechanism is started to check the liveness of the connection: ``` CLIENT SERVER │ *** Handshake *** │ │ │ │ ◄───────────────────────────────────────────────── │ │ 2 │ (ping packet) │ ─────────────────────────────────────────────────► │ │ 3 │ (pong packet) ``` At a given interval (the `pingInterval` value sent in the handshake) the server sends a `ping` packet and the client has a few seconds (the `pingTimeout` value) to send a `pong` packet back. If the server does not receive a `pong` packet back, then it SHOULD consider that the connection is closed. Conversely, if the client does not receive a `ping` packet within `pingInterval + pingTimeout`, then it SHOULD consider that the connection is closed. ### Upgrade By default, the client SHOULD create an HTTP long-polling connection, and then upgrade to better transports if available. To upgrade to WebSocket, the client MUST: - pause the HTTP long-polling transport (no more HTTP request gets sent), to ensure that no packet gets lost - open a WebSocket connection with the same session ID - send a `ping` packet with the string `probe` in the payload The server MUST: - send a `noop` packet to any pending `GET` request (if applicable) to cleanly close HTTP long-polling transport - respond with a `pong` packet with the string `probe` in the payload Finally, the client MUST send a `upgrade` packet to complete the upgrade: ``` CLIENT SERVER │ │ │ GET /engine.io/?EIO=4&transport=websocket&sid=... │ │ ───────────────────────────────────────────────────► │ │ ◄─────────────────────────────────────────────────┘ │ │ HTTP 101 (WebSocket handshake) │ │ │ │ ----- WebSocket frames ----- │ │ ─────────────────────────────────────────────────► │ │ 2probe │ (ping packet) │ ◄───────────────────────────────────────────────── │ │ 3probe │ (pong packet) │ ─────────────────────────────────────────────────► │ │ 5 │ (upgrade packet) │ │ ``` ### Message Once the [handshake](#handshake) is completed, the client and the server can exchange data by including it in a `message` packet. ## Packet encoding The serialization of an Engine.IO packet depends on the type of the payload (plaintext or binary) and on the transport. The character encoding is UTF-8 for plain text and for base64-encoded binary payloads. ### HTTP long-polling Due to the nature of the HTTP long-polling transport, multiple packets might be concatenated in a single payload in order to increase throughput. Format: ``` [][][][...] ``` Example: ``` 4hello\x1e2\x1e4world with: 4 => message packet type hello => message payload \x1e => separator 2 => ping packet type \x1e => separator 4 => message packet type world => message payload ``` The packets are separated by the [record separator character](https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators): `\x1e` Binary payloads MUST be base64-encoded and prefixed with a `b` character: Example: ``` 4hello\x1ebAQIDBA== with: 4 => message packet type hello => message payload \x1e => separator b => binary prefix AQIDBA== => buffer <01 02 03 04> encoded as base64 ``` The client SHOULD use the `maxPayload` value sent during the [handshake](#handshake) to decide how many packets should be concatenated. ### WebSocket Each Engine.IO packet is sent in its own [WebSocket frame](https://datatracker.ietf.org/doc/html/rfc6455#section-5). Format: ``` [] ``` Example: ``` 4hello with: 4 => message packet type hello => message payload (UTF-8 encoded) ``` Binary payloads are sent as is, without modification. ## History ### From v2 to v3 - add support for binary data The [2nd version](https://github.com/socketio/engine.io-protocol/tree/v2) of the protocol is used in Socket.IO `v0.9` and below. The [3rd version](https://github.com/socketio/engine.io-protocol/tree/v3) of the protocol is used in Socket.IO `v1` and `v2`. ### From v3 to v4 - reverse ping/pong mechanism The ping packets are now sent by the server, because the timers set in the browsers are not reliable enough. We suspect that a lot of timeout problems came from timers being delayed on the client-side. - always use base64 when encoding a payload with binary data This change allows to treat all payloads (with or without binary) the same way, without having to take in account whether the client or the current transport supports binary data or not. Please note that this only applies to HTTP long-polling. Binary data is sent in WebSocket frames with no additional transformation. - use a record separator (`\x1e`) instead of counting of characters Counting characters prevented (or at least makes harder) to implement the protocol in other languages, which may not use the UTF-16 encoding. For example, `€` was encoded to `2:4€`, though `Buffer.byteLength('€') === 3`. Note: this assumes the record separator is not used in the data. The 4th version (current) is included in Socket.IO `v3` and above. ## Test suite The test suite in the `test-suite/` directory lets you check the compliance of a server implementation. Usage: - in Node.js: `npm ci && npm test` - in a browser: simply open the `index.html` file in your browser For reference, here is expected configuration for the JavaScript server to pass all tests: ```js import { listen } from "engine.io"; const server = listen(3000, { pingInterval: 300, pingTimeout: 200, maxPayload: 1e6, cors: { origin: "*" } }); server.on("connection", socket => { socket.on("data", (...args) => { socket.send(...args); }); }); ``` ================================================ FILE: docs/engine.io-protocol/v4-test-suite/.gitignore ================================================ node_modules ================================================ FILE: docs/engine.io-protocol/v4-test-suite/index.html ================================================ Test suite for the Engine.IO protocol
================================================ FILE: docs/engine.io-protocol/v4-test-suite/node-imports.js ================================================ import fetch from "node-fetch"; import { WebSocket } from "ws"; import chai from "chai"; import chaiString from "chai-string"; chai.use(chaiString); globalThis.fetch = fetch; globalThis.WebSocket = WebSocket; globalThis.chai = chai; ================================================ FILE: docs/engine.io-protocol/v4-test-suite/package.json ================================================ { "name": "engine.io-protocol-test-suite", "version": "0.0.1", "private": true, "type": "module", "scripts": { "format": "prettier -w *.js", "test": "mocha test-suite.js" }, "devDependencies": { "chai": "^4.3.6", "chai-string": "^1.5.0", "mocha": "^9.2.1", "node-fetch": "^3.2.0", "prettier": "^2.5.1", "ws": "^8.5.0" } } ================================================ FILE: docs/engine.io-protocol/v4-test-suite/test-suite.js ================================================ const isNodejs = typeof window === "undefined"; if (isNodejs) { // make the tests runnable in both the browser and Node.js await import("./node-imports.js"); } const { expect } = chai; const URL = "http://localhost:3000"; const WS_URL = URL.replace("http", "ws"); const PING_INTERVAL = 300; const PING_TIMEOUT = 200; function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } function createWebSocket(url) { const socket = new WebSocket(url); socket._eventBuffer = {}; socket._pendingPromises = {}; for (const eventType of ["open", "close", "message"]) { socket._eventBuffer[eventType] = []; socket._pendingPromises[eventType] = []; socket.addEventListener(eventType, (event) => { if (socket._pendingPromises[eventType].length) { socket._pendingPromises[eventType].shift()(event); } else { socket._eventBuffer[eventType].push(event); } }); } return socket; } function waitFor(socket, eventType) { if (socket._eventBuffer[eventType].length) { return Promise.resolve(socket._eventBuffer[eventType].shift()); } else { return new Promise((resolve) => { socket._pendingPromises[eventType].push(resolve); }); } } async function initLongPollingSession() { const response = await fetch(`${URL}/engine.io/?EIO=4&transport=polling`); const content = await response.text(); return JSON.parse(content.substring(1)).sid; } describe("Engine.IO protocol", () => { describe("handshake", () => { describe("HTTP long-polling", () => { it("successfully opens a session", async () => { const response = await fetch( `${URL}/engine.io/?EIO=4&transport=polling` ); expect(response.status).to.eql(200); const content = await response.text(); expect(content).to.startsWith("0"); const value = JSON.parse(content.substring(1)); expect(value).to.have.all.keys( "sid", "upgrades", "pingInterval", "pingTimeout", "maxPayload" ); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql(["websocket"]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.eql(1000000); }); it("fails with an invalid 'EIO' query parameter", async () => { const response = await fetch(`${URL}/engine.io/?transport=polling`); expect(response.status).to.eql(400); const response2 = await fetch( `${URL}/engine.io/?EIO=abc&transport=polling` ); expect(response2.status).to.eql(400); }); it("fails with an invalid 'transport' query parameter", async () => { const response = await fetch(`${URL}/engine.io/?EIO=4`); expect(response.status).to.eql(400); const response2 = await fetch(`${URL}/engine.io/?EIO=4&transport=abc`); expect(response2.status).to.eql(400); }); it("fails with an invalid request method", async () => { const response = await fetch( `${URL}/engine.io/?EIO=4&transport=polling`, { method: "post", } ); expect(response.status).to.eql(400); const response2 = await fetch( `${URL}/engine.io/?EIO=4&transport=polling`, { method: "put", } ); expect(response2.status).to.eql(400); }); }); describe("WebSocket", () => { it("successfully opens a session", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("0"); const value = JSON.parse(data.substring(1)); expect(value).to.have.all.keys( "sid", "upgrades", "pingInterval", "pingTimeout", "maxPayload" ); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql([]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.eql(1000000); socket.close(); }); it("fails with an invalid 'EIO' query parameter", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?transport=websocket` ); if (isNodejs) { socket.on("error", () => {}); } await waitFor(socket, "close"); const socket2 = createWebSocket( `${WS_URL}/engine.io/?EIO=abc&transport=websocket` ); if (isNodejs) { socket2.on("error", () => {}); } await waitFor(socket2, "close"); }); it("fails with an invalid 'transport' query parameter", async () => { const socket = createWebSocket(`${WS_URL}/engine.io/?EIO=4`); if (isNodejs) { socket.on("error", () => {}); } await waitFor(socket, "close"); const socket2 = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=abc` ); if (isNodejs) { socket2.on("error", () => {}); } await waitFor(socket2, "close"); }); }); }); describe("message", () => { describe("HTTP long-polling", () => { it("sends and receives a payload containing one plain text packet", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "4hello", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("4hello"); }); it("sends and receives a payload containing several plain text packets", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "4test1\x1e4test2\x1e4test3", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("4test1\x1e4test2\x1e4test3"); }); it("sends and receives a payload containing plain text and binary packets", async () => { const sid = await initLongPollingSession(); const pushResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "4hello\x1ebAQIDBA==", } ); expect(pushResponse.status).to.eql(200); const postContent = await pushResponse.text(); expect(postContent).to.eql("ok"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("4hello\x1ebAQIDBA=="); }); it("closes the session upon invalid packet format", async () => { const sid = await initLongPollingSession(); try { const pushResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "abc", } ); expect(pushResponse.status).to.eql(400); } catch (e) { // node-fetch throws when the request is closed abnormally } const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); it("closes the session upon duplicate poll requests", async () => { const sid = await initLongPollingSession(); const pollResponses = await Promise.all([ fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`), sleep(5).then(() => fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}&t=burst`)), ]); expect(pollResponses[0].status).to.eql(200); const content = await pollResponses[0].text(); expect(content).to.eql("1"); // the Node.js implementation uses HTTP 500 (Internal Server Error), but HTTP 400 seems more suitable expect(pollResponses[1].status).to.be.oneOf([400, 500]); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); it("closes the session upon cancelled polling request", async () => { const sid = await initLongPollingSession(); const controller = new AbortController(); fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { signal: controller.signal, }).catch(() => {}); await sleep(5); controller.abort(); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, ); expect(pollResponse.status).to.eql(400); }); }); describe("WebSocket", () => { it("sends and receives a plain text packet", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); await waitFor(socket, "open"); await waitFor(socket, "message"); // handshake socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); socket.close(); }); it("sends and receives a binary packet", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); socket.binaryType = "arraybuffer"; await waitFor(socket, "message"); // handshake socket.send(Uint8Array.from([1, 2, 3, 4])); const { data } = await waitFor(socket, "message"); expect(data).to.eql(Uint8Array.from([1, 2, 3, 4]).buffer); socket.close(); }); it("closes the session upon invalid packet format", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // handshake socket.send("abc"); await waitFor(socket, "close"); socket.close(); }); }); }); describe("heartbeat", function () { this.timeout(5000); describe("HTTP long-polling", () => { it("sends ping/pong packets", async () => { const sid = await initLongPollingSession(); for (let i = 0; i < 3; i++) { const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("2"); const pushResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "3", } ); expect(pushResponse.status).to.eql(200); } }); it("closes the session upon ping timeout", async () => { const sid = await initLongPollingSession(); await sleep(PING_INTERVAL + PING_TIMEOUT); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); }); describe("WebSocket", () => { it("sends ping/pong packets", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // handshake for (let i = 0; i < 3; i++) { const { data } = await waitFor(socket, "message"); expect(data).to.eql("2"); socket.send("3"); } socket.close(); }); it("closes the session upon ping timeout", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); await waitFor(socket, "close"); // handshake }); }); }); describe("close", () => { describe("HTTP long-polling", () => { it("forcefully closes the session", async () => { const sid = await initLongPollingSession(); const [pollResponse] = await Promise.all([ fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`), fetch(`${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "1", }), ]); expect(pollResponse.status).to.eql(200); const pullContent = await pollResponse.text(); expect(pullContent).to.eql("6"); const pollResponse2 = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse2.status).to.eql(400); }); }); describe("WebSocket", () => { it("forcefully closes the session", async () => { const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // handshake socket.send("1"); await waitFor(socket, "close"); }); }); }); describe("upgrade", () => { it("successfully upgrades from HTTP long-polling to WebSocket", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); // send probe socket.send("2probe"); const probeResponse = await waitFor(socket, "message"); expect(probeResponse.data).to.eql("3probe"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("6"); // "noop" packet to cleanly end the HTTP long-polling request // complete upgrade socket.send("5"); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); it("ignores HTTP requests with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); await waitFor(socket, "message"); // "3probe" socket.send("5"); const pollResponse = await fetch( `${URL}/engine.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); it("ignores WebSocket connection with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); await waitFor(socket, "message"); // "3probe" socket.send("5"); const socket2 = createWebSocket( `${WS_URL}/engine.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket2, "close"); socket.send("4hello"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("4hello"); }); }); }); ================================================ FILE: docs/socket.io-protocol/v3.md ================================================ # socket.io-protocol This document describes the Socket.IO protocol. For a reference JavaScript implementation, take a look at [socket.io-parser](https://github.com/socketio/socket.io-parser), [socket.io-client](https://github.com/socketio/socket.io-client) and [socket.io](https://github.com/socketio/socket.io). ## Table of Contents - [Protocol version](#protocol-version) - [Packet format](#packet-format) - [Packet types](#packet-types) - [CONNECT](#0---connect) - [DISCONNECT](#1---disconnect) - [EVENT](#2---event) - [ACK](#3---ack) - [ERROR](#4---error) - [BINARY_EVENT](#5---binary_event) - [Packet encoding](#packet-encoding) - [Encoding format](#encoding-format) - [Examples](#examples) - [Exchange protocol](#exchange-protocol) - [Connection to the default namespace](#connection-to-the-default-namespace) - [Connection to a non-default namespace](#connection-to-a-non-default-namespace) - [Disconnection from a non-default namespace](#disconnection-from-a-non-default-namespace) - [Acknowledgement](#acknowledgement) - [History](#history) - [Difference between v3 and v2](#difference-between-v3-and-v2) - [Difference between v2 and v1](#difference-between-v2-and-v1) - [Initial revision](#initial-revision) ## Protocol version This is the revision **3** of the Socket.IO protocol, included in ̀`socket.io@1.0.0...1.0.2`. The 4th revision (included in ̀`socket.io@1.0.3...latest`) can be found here: https://github.com/socketio/socket.io-protocol/tree/master Both the 1st and the 2nd revisions were part of the work towards Socket.IO 1.0 but were never included in a Socket.IO release. It is built on top of the [3rd](https://github.com/socketio/engine.io-protocol/tree/v3) revision of the Engine.IO protocol. While the Engine.IO protocol describes the low-level plumbing with WebSocket and HTTP long-polling, the Socket.IO protocol adds another layer above in order to provide the following features: - multiplexing (what we call [Namespace](https://socket.io/docs/namespaces/)) Example of the Javascript API: ```js // server-side const nsp = io.of("/admin"); nsp.on("connect", socket => {}); // client-side const socket1 = io(); // default namespace const socket2 = io("/admin"); socket2.on("connect", () => {}); ``` - acknowledgement of packets Example of the Javascript API: ```js // on one side socket.emit("hello", 1, () => { console.log("received"); }); // on the other side socket.on("hello", (a, cb) => { cb(); }); ``` ## Packet format A packet contains the following fields: - a type (integer, see [below](#packet-types)) - a namespace (string) - optionally, a payload (string | Array) - optionally, an acknowledgment id (integer) ## Packet types ### 0 - CONNECT This event is sent: - by the client when requesting access to a namespace - by the server when accepting the connection to a namespace It does not contain any payload nor acknowledgement id. Example: ```json { "type": 0, "nsp": "/admin" } ``` The client may include additional information (i.e. for authentication purpose) in the namespace field. Example: ```json { "type": 0, "nsp": "/admin?token=1234&uid=abcd" } ``` #### 1 - DISCONNECT This event is used when one side wants to disconnect from a namespace. It does not contain any payload nor acknowledgement id. Example: ```json { "type": 1, "nsp": "/admin" } ``` #### 2 - EVENT This event is used when one side wants to transmit some data (without binary) to the other side. It does contain a payload, and an optional acknowledgement id. Example: ```json { "type": 2, "nsp": "/", "data": ["hello", 1] } ``` With an acknowledgment id: ```json { "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 } ``` #### 3 - ACK This event is used when one side has received an EVENT or a BINARY_EVENT with an acknowledgement id. It contains the acknowledgement id received in the previous packet, and may contain a payload (without binary). ```json { "type": 3, "nsp": "/admin", "data": [], "id": 456 } ``` #### 4 - ERROR This event is sent by the server when the connection to a namespace is refused. It may contain a payload indicating the reason of the refusal. Example: ```json { "type": 4, "nsp": "/admin", "data": "Not authorized" } ``` #### 5 - BINARY_EVENT This event is used when one side wants to transmit some data (including binary) to the other side. It does contain a payload, and an optional acknowledgement id. Example: ``` { "type": 5, "nsp": "/", "data": ["hello", ] } ``` With an acknowledgment id: ``` { "type": 5, "nsp": "/admin", "data": ["project:delete", ], "id": 456 } ``` ## Packet encoding This section details the encoding used by the default parser which is included in Socket.IO server and client, and whose source can be found [here](https://github.com/socketio/socket.io-parser). The JS server and client implementations also supports custom parsers, which have different tradeoffs and may benefit to certain kind of applications. Please see [socket.io-json-parser](https://github.com/darrachequesne/socket.io-json-parser) or [socket.io-msgpack-parser](https://github.com/darrachequesne/socket.io-msgpack-parser) for example. Please also note that each Socket.IO packet is sent as a Engine.IO `message` packet (more information [here](https://github.com/socketio/engine.io-protocol)), so the encoded result will be prefixed by `4` when sent over the wire (in the request/response body with HTTP long-polling, or in the WebSocket frame). ### Encoding format ``` [<# of binary attachments>-][,][][JSON-stringified payload without binary] + binary attachments extracted ``` Note: - the namespace is only included if it is different from the default namespace (`/`) ### Examples - `CONNECT` packet for the default namespace ```json { "type": 0, "nsp": "/" } ``` is encoded to `0` - `CONNECT` packet for the `/admin` namespace ```json { "type": 0, "nsp": "/admin" } ``` is encoded to `0/admin` - `DISCONNECT` packet for the `/admin` namespace ```json { "type": 1, "nsp": "/admin" } ``` is encoded to `1/admin` - `EVENT` packet ```json { "type": 2, "nsp": "/", "data": ["hello", 1] } ``` is encoded to `2["hello",1]` - `EVENT` packet with an acknowledgement id ```json { "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 } ``` is encoded to `2/admin,456["project:delete",123]` - `ACK` packet ```json { "type": 3, "nsp": "/admin", "data": [], "id": 456 } ``` is encoded to `3/admin,456[]` - `ERROR` packet ```json { "type": 4, "nsp": "/admin", "data": "Not authorized" } ``` is encoded to `4/admin,"Not authorized"` - `BINARY_EVENT` packet ``` { "type": 5, "nsp": "/", "data": ["hello", ] } ``` is encoded to `51-["hello",{"_placeholder":true,"num":0}]` + `` - `BINARY_EVENT` packet with an acknowledgement id ``` { "type": 5, "nsp": "/admin", "data": ["project:delete", ], "id": 456 } ``` is encoded to `51-/admin,456["project:delete",{"_placeholder":true,"num":0}]` + `` ## Exchange protocol ### Connection to the default namespace The server always send a `CONNECT` packet for the default namespace (`/`) when the connection is established. That is, even if the client requests access to a non-default namespace, it will receive a `CONNECT` packet for the default namespace first. ``` Server > { type: CONNECT, nsp: "/" } ``` No response is expected from the client. ### Connection to a non-default namespace ``` Client > { type: CONNECT, nsp: "/admin" } Server > { type: CONNECT, nsp: "/admin" } (if the connection is successful) or Server > { type: ERROR, nsp: "/admin", data: "Not authorized" } ``` ### Disconnection from a non-default namespace ``` Client > { type: DISCONNECT, nsp: "/admin" } ``` And vice versa. No response is expected from the other-side. ### Acknowledgement ``` Client > { type: EVENT, nsp: "/admin", data: ["hello"], id: 456 } Server > { type: ACK, nsp: "/admin", data: [], id: 456 } ``` And vice versa. ## History ### Difference between v3 and v2 - remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) ### Difference between v2 and v1 - add a BINARY_EVENT packet type This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The BINARY_EVENT packets were encoded with [msgpack](https://msgpack.org/). ### Initial revision This first revision was the result of the split between the Engine.IO protocol (low-level plumbing with WebSocket / HTTP long-polling, heartbeat) and the Socket.IO protocol. It was never included in a Socket.IO release, but paved the way for the next iterations. ## License MIT ================================================ FILE: docs/socket.io-protocol/v4.md ================================================ # socket.io-protocol This document describes the Socket.IO protocol. For a reference JavaScript implementation, take a look at [socket.io-parser](https://github.com/socketio/socket.io-parser), [socket.io-client](https://github.com/socketio/socket.io-client) and [socket.io](https://github.com/socketio/socket.io). ## Table of Contents - [Protocol version](#protocol-version) - [Packet format](#packet-format) - [Packet types](#packet-types) - [CONNECT](#0---connect) - [DISCONNECT](#1---disconnect) - [EVENT](#2---event) - [ACK](#3---ack) - [ERROR](#4---error) - [BINARY_EVENT](#5---binary_event) - [BINARY_ACK](#6---binary_ack) - [Packet encoding](#packet-encoding) - [Encoding format](#encoding-format) - [Examples](#examples) - [Exchange protocol](#exchange-protocol) - [Connection to the default namespace](#connection-to-the-default-namespace) - [Connection to a non-default namespace](#connection-to-a-non-default-namespace) - [Disconnection from a non-default namespace](#disconnection-from-a-non-default-namespace) - [Acknowledgement](#acknowledgement) - [Sample session](#sample-session) - [History](#history) - [Difference between v4 and v3](#difference-between-v4-and-v3) - [Difference between v3 and v2](#difference-between-v3-and-v2) - [Difference between v2 and v1](#difference-between-v2-and-v1) - [Initial revision](#initial-revision) ## Protocol version This is the revision **4** of the Socket.IO protocol, included in `socket.io@1.0.3...latest`. The 3rd revision (included in `socket.io@1.0.0...1.0.2`) can be found here: https://github.com/socketio/socket.io-protocol/tree/v3 Both the 1st and the 2nd revisions were part of the work towards Socket.IO 1.0 but were never included in a Socket.IO release. It is built on top of the [3rd](https://github.com/socketio/engine.io-protocol/tree/v3) revision of the Engine.IO protocol. While the Engine.IO protocol describes the low-level plumbing with WebSocket and HTTP long-polling, the Socket.IO protocol adds another layer above in order to provide the following features: - multiplexing (what we call [Namespace](https://socket.io/docs/namespaces/)) Example of the Javascript API: ```js // server-side const nsp = io.of("/admin"); nsp.on("connect", socket => {}); // client-side const socket1 = io(); // default namespace const socket2 = io("/admin"); socket2.on("connect", () => {}); ``` - acknowledgement of packets Example of the Javascript API: ```js // on one side socket.emit("hello", 1, () => { console.log("received"); }); // on the other side socket.on("hello", (a, cb) => { cb(); }); ``` ## Packet format A packet contains the following fields: - a type (integer, see [below](#packet-types)) - a namespace (string) - optionally, a payload (string | Array) - optionally, an acknowledgment id (integer) ## Packet types ### 0 - CONNECT This event is sent: - by the client when requesting access to a namespace - by the server when accepting the connection to a namespace It does not contain any payload nor acknowledgement id. Example: ```json { "type": 0, "nsp": "/admin" } ``` The client may include additional information (i.e. for authentication purpose) in the namespace field. Example: ```json { "type": 0, "nsp": "/admin?token=1234&uid=abcd" } ``` #### 1 - DISCONNECT This event is used when one side wants to disconnect from a namespace. It does not contain any payload nor acknowledgement id. Example: ```json { "type": 1, "nsp": "/admin" } ``` #### 2 - EVENT This event is used when one side wants to transmit some data (without binary) to the other side. It does contain a payload, and an optional acknowledgement id. Example: ```json { "type": 2, "nsp": "/", "data": ["hello", 1] } ``` With an acknowledgment id: ```json { "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 } ``` #### 3 - ACK This event is used when one side has received an EVENT or a BINARY_EVENT with an acknowledgement id. It contains the acknowledgement id received in the previous packet, and may contain a payload (without binary). ```json { "type": 3, "nsp": "/admin", "data": [], "id": 456 } ``` #### 4 - ERROR This event is sent by the server when the connection to a namespace is refused. It may contain a payload indicating the reason of the refusal. Example: ```json { "type": 4, "nsp": "/admin", "data": "Not authorized" } ``` #### 5 - BINARY_EVENT This event is used when one side wants to transmit some data (including binary) to the other side. It does contain a payload, and an optional acknowledgement id. Example: ``` { "type": 5, "nsp": "/", "data": ["hello", ] } ``` With an acknowledgment id: ``` { "type": 5, "nsp": "/admin", "data": ["project:delete", ], "id": 456 } ``` #### 6 - BINARY_ACK This event is used when one side has received an EVENT or a BINARY_EVENT with an acknowledgement id. It contains the acknowledgement id received in the previous packet, and contain a payload including binary. Example: ``` { "type": 6, "nsp": "/admin", "data": [], "id": 456 } ``` ## Packet encoding This section details the encoding used by the default parser which is included in Socket.IO server and client, and whose source can be found [here](https://github.com/socketio/socket.io-parser). The JS server and client implementations also supports custom parsers, which have different tradeoffs and may benefit to certain kind of applications. Please see [socket.io-json-parser](https://github.com/darrachequesne/socket.io-json-parser) or [socket.io-msgpack-parser](https://github.com/darrachequesne/socket.io-msgpack-parser) for example. Please also note that each Socket.IO packet is sent as a Engine.IO `message` packet (more information [here](https://github.com/socketio/engine.io-protocol)), so the encoded result will be prefixed by `4` when sent over the wire (in the request/response body with HTTP long-polling, or in the WebSocket frame). ### Encoding format ``` [<# of binary attachments>-][,][][JSON-stringified payload without binary] + binary attachments extracted ``` Note: - the namespace is only included if it is different from the default namespace (`/`) ### Examples - `CONNECT` packet for the default namespace ```json { "type": 0, "nsp": "/" } ``` is encoded to `0` - `CONNECT` packet for the `/admin` namespace ```json { "type": 0, "nsp": "/admin" } ``` is encoded to `0/admin,` - `DISCONNECT` packet for the `/admin` namespace ```json { "type": 1, "nsp": "/admin" } ``` is encoded to `1/admin,` - `EVENT` packet ```json { "type": 2, "nsp": "/", "data": ["hello", 1] } ``` is encoded to `2["hello",1]` - `EVENT` packet with an acknowledgement id ```json { "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 } ``` is encoded to `2/admin,456["project:delete",123]` - `ACK` packet ```json { "type": 3, "nsp": "/admin", "data": [], "id": 456 } ``` is encoded to `3/admin,456[]` - `ERROR` packet ```json { "type": 4, "nsp": "/admin", "data": "Not authorized" } ``` is encoded to `4/admin,"Not authorized"` - `BINARY_EVENT` packet ``` { "type": 5, "nsp": "/", "data": ["hello", ] } ``` is encoded to `51-["hello",{"_placeholder":true,"num":0}]` + `` - `BINARY_EVENT` packet with an acknowledgement id ``` { "type": 5, "nsp": "/admin", "data": ["project:delete", ], "id": 456 } ``` is encoded to `51-/admin,456["project:delete",{"_placeholder":true,"num":0}]` + `` - `BINARY_ACK` packet ``` { "type": 6, "nsp": "/admin", "data": [], "id": 456 } ``` is encoded to `61-/admin,456[{"_placeholder":true,"num":0}]` + `` ## Exchange protocol ### Connection to the default namespace The server always send a `CONNECT` packet for the default namespace (`/`) when the connection is established. That is, even if the client requests access to a non-default namespace, it will receive a `CONNECT` packet for the default namespace first. ``` Server > { type: CONNECT, nsp: "/" } ``` No response is expected from the client. ### Connection to a non-default namespace ``` Client > { type: CONNECT, nsp: "/admin" } Server > { type: CONNECT, nsp: "/admin" } (if the connection is successful) or Server > { type: ERROR, nsp: "/admin", data: "Not authorized" } ``` ### Disconnection from a non-default namespace ``` Client > { type: DISCONNECT, nsp: "/admin" } ``` And vice versa. No response is expected from the other-side. ### Acknowledgement ``` Client > { type: EVENT, nsp: "/admin", data: ["hello"], id: 456 } Server > { type: ACK, nsp: "/admin", data: [], id: 456 } or Server > { type: BINARY_ACK, nsp: "/admin", data: [ ], id: 456 } ``` And vice versa. ## Sample session Here is an example of what is sent over the wire when combining both the Engine.IO and the Socket.IO protocols. - Request n°1 (open packet) ``` GET /socket.io/?EIO=3&transport=polling&t=N8hyd6w < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 96:0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40 ``` Details: ``` 96 => number of characters (not bytes) of the first message : => separator 0 => Engine.IO "open" packet type {"sid":... => the Engine.IO handshake data 2 => number of characters of the 2nd message : => separator 4 => Engine.IO "message" packet type 0 => Socket.IO "CONNECT" packet type ``` Note: the `t` query param is used to ensure that the request is not cached by the browser. - Request n°2 (message in): `socket.emit('hey', 'Jude')` is executed on the server: ``` GET /socket.io/?EIO=3&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 16:42["hey","Jude"] ``` Details: ``` 16 => number of characters : => separator 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type [...] => content ``` - Request n°3 (message out) `socket.emit('hello'); socket.emit('world');` is executed on the client: ``` POST /socket.io/?EIO=3&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC > Content-Type: text/plain; charset=UTF-8 11:42["hello"]11:42["world"] < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 ok ``` Details: ``` 11 => number of characters of the 1st packet : => separator 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type ["hello"] => the 1st content 11 => number of characters of the 2nd packet : => separator 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type ["world"] => the 2nd content ``` - Request n°4 (WebSocket upgrade) ``` GET /socket.io/?EIO=3&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 101 Switching Protocols ``` WebSocket frames: ``` < 2probe => Engine.IO probe request > 3probe => Engine.IO probe response > 5 => Engine.IO "upgrade" packet type > 42["hello"] > 42["world"] > 40/admin, => request access to the admin namespace (Socket.IO "CONNECT" packet) < 40/admin, => grant access to the admin namespace > 42/admin,1["tellme"] => Socket.IO "EVENT" packet with acknowledgement < 461-/admin,1[{"_placeholder":true,"num":0}] => Socket.IO "BINARY_ACK" packet with a placeholder < => the binary attachment (sent in the following frame) ... after a while without message > 2 => Engine.IO "ping" packet type < 3 => Engine.IO "pong" packet type > 1 => Engine.IO "close" packet type ``` ## History ### Difference between v4 and v3 - add a `BINARY_ACK` packet type Previously, an `ACK` packet was always treated as if it may contain binary objects, with recursive search for such objects, which could hurt performance. ### Difference between v3 and v2 - remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) ### Difference between v2 and v1 - add a `BINARY_EVENT` packet type This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The `BINARY_EVENT` packets were encoded with [msgpack](https://msgpack.org/). ### Initial revision This first revision was the result of the split between the Engine.IO protocol (low-level plumbing with WebSocket / HTTP long-polling, heartbeat) and the Socket.IO protocol. It was never included in a Socket.IO release, but paved the way for the next iterations. ## License MIT ================================================ FILE: docs/socket.io-protocol/v5-current.md ================================================ # Socket.IO Protocol This document describes the 5th version of the Socket.IO protocol. **Table of content** - [Introduction](#introduction) - [Exchange protocol](#exchange-protocol) - [Connection to a namespace](#connection-to-a-namespace) - [Sending and receiving data](#sending-and-receiving-data) - [Acknowledgement](#acknowledgement) - [Disconnection from a namespace](#disconnection-from-a-namespace) - [Packet encoding](#packet-encoding) - [Format](#format) - [Examples](#examples) - [Connection to a namespace](#connection-to-a-namespace-1) - [Sending and receiving data](#sending-and-receiving-data-1) - [Acknowledgement](#acknowledgement-1) - [Disconnection from a namespace](#disconnection-from-a-namespace-1) - [Sample session](#sample-session) - [History](#history) - [Difference between v5 and v4](#difference-between-v5-and-v4) - [Difference between v4 and v3](#difference-between-v4-and-v3) - [Difference between v3 and v2](#difference-between-v3-and-v2) - [Difference between v2 and v1](#difference-between-v2-and-v1) - [Initial revision](#initial-revision) - [Test suite](#test-suite) ## Introduction The Socket.IO protocol enables [full-duplex](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX) and low-overhead communication between a client and a server. It is built on top of [the Engine.IO protocol](https://github.com/socketio/engine.io-protocol), which handles the low-level plumbing with WebSocket and HTTP long-polling. The Socket.IO protocol adds the following features: - multiplexing (referred as ["namespace"](https://socket.io/docs/v4/namespaces) in the Socket.IO jargon) Example with the JavaScript API: *Server* ```js // declare the namespace const namespace = io.of("/admin"); // handle the connection to the namespace namespace.on("connection", (socket) => { // ... }); ``` *Client* ```js // reach the main namespace const socket1 = io(); // reach the "/admin" namespace (with the same underlying WebSocket connection) const socket2 = io("/admin"); // handle the connection to the namespace socket2.on("connect", () => { // ... }); ``` - acknowledgement of packets Example with the JavaScript API: ```js // on one side socket.emit("hello", "foo", (arg) => { console.log("received", arg); }); // on the other side socket.on("hello", (arg, ack) => { ack("bar"); }); ``` The reference implementation is written in [TypeScript](https://www.typescriptlang.org/): - server: https://github.com/socketio/socket.io - client: https://github.com/socketio/socket.io-client ## Exchange protocol A Socket.IO packet contains the following fields: - a packet type (integer) - a namespace (string) - optionally, a payload (Object | Array) - optionally, an acknowledgment id (integer) Here is the list of available packet types: | Type | ID | Usage | |---------------|-----|---------------------------------------------------------------------------------------| | CONNECT | 0 | Used during the [connection to a namespace](#connection-to-a-namespace). | | DISCONNECT | 1 | Used when [disconnecting from a namespace](#disconnection-from-a-namespace). | | EVENT | 2 | Used to [send data](#sending-and-receiving-data) to the other side. | | ACK | 3 | Used to [acknowledge](#acknowledgement) an event. | | CONNECT_ERROR | 4 | Used during the [connection to a namespace](#connection-to-a-namespace). | | BINARY_EVENT | 5 | Used to [send binary data](#sending-and-receiving-data) to the other side. | | BINARY_ACK | 6 | Used to [acknowledge](#acknowledgement) an event (the response includes binary data). | ### Connection to a namespace At the beginning of a Socket.IO session, the client MUST send a `CONNECT` packet: The server MUST respond with either: - a `CONNECT` packet if the connection is successful, with the session ID in the payload - or a `CONNECT_ERROR` packet if the connection is not allowed ``` CLIENT SERVER │ ───────────────────────────────────────────────────────► │ │ { type: CONNECT, namespace: "/" } │ │ ◄─────────────────────────────────────────────────────── │ │ { type: CONNECT, namespace: "/", data: { sid: "..." } } │ ``` If the server does not receive a `CONNECT` packet first, then it MUST close the connection immediately. A client MAY be connected to multiple namespaces at the same time, with the same underlying WebSocket connection. Examples: - with the main namespace (named `"/"`) ``` Client > { type: CONNECT, namespace: "/" } Server > { type: CONNECT, namespace: "/", data: { sid: "wZX3oN0bSVIhsaknAAAI" } } ``` - with a custom namespace ``` Client > { type: CONNECT, namespace: "/admin" } Server > { type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } } ``` - with an additional payload ``` Client > { type: CONNECT, namespace: "/admin", data: { "token": "123" } } Server > { type: CONNECT, namespace: "/admin", data: { sid: "iLnRaVGHY4B75TeVAAAB" } } ``` - in case the connection is refused ``` Client > { type: CONNECT, namespace: "/" } Server > { type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } } ``` ### Sending and receiving data Once the [connection to a namespace](#connection-to-a-namespace) is established, the client and the server can begin exchanging data: ``` CLIENT SERVER │ ───────────────────────────────────────────────────────► │ │ { type: EVENT, namespace: "/", data: ["foo"] } │ │ │ │ ◄─────────────────────────────────────────────────────── │ │ { type: EVENT, namespace: "/", data: ["bar"] } │ ``` The payload is mandatory and MUST be a non-empty array. If that's not the case, then the receiver MUST close the connection. Examples: - with the main namespace ``` Client > { type: EVENT, namespace: "/", data: ["foo"] } ``` - with a custom namespace ``` Server > { type: EVENT, namespace: "/admin", data: ["bar"] } ``` - with binary data ``` Client > { type: BINARY_EVENT, namespace: "/", data: ["baz", > ] } ``` ### Acknowledgement The sender MAY include an event ID in order to request an acknowledgement from the receiver: ``` CLIENT SERVER │ ───────────────────────────────────────────────────────► │ │ { type: EVENT, namespace: "/", data: ["foo"], id: 12 } │ │ ◄─────────────────────────────────────────────────────── │ │ { type: ACK, namespace: "/", data: ["bar"], id: 12 } │ ``` The receiver MUST respond with an `ACK` packet with the same event ID. The payload is mandatory and MUST be an array (possibly empty). Examples: - with the main namespace ``` Client > { type: EVENT, namespace: "/", data: ["foo"], id: 12 } Server > { type: ACK, namespace: "/", data: [], id: 12 } ``` - with a custom namespace ``` Server > { type: EVENT, namespace: "/admin", data: ["foo"], id: 13 } Client > { type: ACK, namespace: "/admin", data: ["bar"], id: 13 } ``` - with binary data ``` Client > { type: BINARY_EVENT, namespace: "/", data: ["foo", ], id: 14 } Server > { type: ACK, namespace: "/", data: ["bar"], id: 14 } or Server > { type: EVENT, namespace: "/", data: ["foo" ], id: 15 } Client > { type: BINARY_ACK, namespace: "/", data: ["bar", ], id: 15 } ``` ### Disconnection from a namespace At any time, one side can end the connection to a namespace by sending a `DISCONNECT` packet: ``` CLIENT SERVER │ ───────────────────────────────────────────────────────► │ │ { type: DISCONNECT, namespace: "/" } │ ``` No response is expected from the other side. The low-level connection MAY be kept alive if the client is connected to another namespace. ## Packet encoding This section details the encoding used by the default parser which is included in Socket.IO server and client, and whose source can be found [here](https://github.com/socketio/socket.io-parser). The JavaScript server and client implementations also supports custom parsers, which have different tradeoffs and may benefit to certain kind of applications. Please see [socket.io-json-parser](https://github.com/socketio/socket.io-json-parser) or [socket.io-msgpack-parser](https://github.com/socketio/socket.io-msgpack-parser) for example. Please also note that each Socket.IO packet is sent as a Engine.IO `message` packet (more information [here](https://github.com/socketio/engine.io-protocol)), so the encoded result will be prefixed by the character `"4"` when sent over the wire (in the request/response body with HTTP long-polling, or in the WebSocket frame). ### Format ``` [<# of binary attachments>-][,][][JSON-stringified payload without binary] + binary attachments extracted ``` Note: the namespace is only included if it is different from the main namespace (`/`) ### Examples #### Connection to a namespace - with the main namespace *Packet* ``` { type: CONNECT, namespace: "/" } ``` *Encoded* ``` 0 ``` - with a custom namespace *Packet* ``` { type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } } ``` *Encoded* ``` 0/admin,{"sid":"oSO0OpakMV_3jnilAAAA"} ``` - in case the connection is refused *Packet* ``` { type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } } ``` *Encoded* ``` 4{"message":"Not authorized"} ``` #### Sending and receiving data - with the main namespace *Packet* ``` { type: EVENT, namespace: "/", data: ["foo"] } ``` *Encoded* ``` 2["foo"] ``` - with a custom namespace *Packet* ``` { type: EVENT, namespace: "/admin", data: ["bar"] } ``` *Encoded* ``` 2/admin,["bar"] ``` - with binary data *Packet* ``` { type: BINARY_EVENT, namespace: "/", data: ["baz", > ] } ``` *Encoded* ``` 51-["baz",{"_placeholder":true,"num":0}] + > ``` - with multiple attachments *Packet* ``` { type: BINARY_EVENT, namespace: "/admin", data: ["baz", >, > ] } ``` *Encoded* ``` 52-/admin,["baz",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}] + > + > ``` Please remember that each Socket.IO packet is wrapped in a Engine.IO `message` packet, so they will be prefixed by the character `"4"` when sent over the wire. Example: `{ type: EVENT, namespace: "/", data: ["foo"] }` will be sent as `42["foo"]` #### Acknowledgement - with the main namespace *Packet* ``` { type: EVENT, namespace: "/", data: ["foo"], id: 12 } ``` *Encoded* ``` 212["foo"] ``` - with a custom namespace *Packet* ``` { type: ACK, namespace: "/admin", data: ["bar"], id: 13 } ``` *Encoded* ``` 3/admin,13["bar"]` ``` - with binary data *Packet* ``` { type: BINARY_ACK, namespace: "/", data: ["bar", >], id: 15 } ``` *Encoded* ``` 61-15["bar",{"_placeholder":true,"num":0}] + > ``` #### Disconnection from a namespace - with the main namespace *Packet* ``` { type: DISCONNECT, namespace: "/" } ``` *Encoded* ``` 1 ``` - with a custom namespace ``` { type: DISCONNECT, namespace: "/admin" } ``` *Encoded* ``` 1/admin, ``` ## Sample session Here is an example of what is sent over the wire when combining both the Engine.IO and the Socket.IO protocols. - Request n°1 (open packet) ``` GET /socket.io/?EIO=4&transport=polling&t=N8hyd6w < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000} ``` Details: ``` 0 => Engine.IO "open" packet type {"sid":... => the Engine.IO handshake data ``` Note: the `t` query param is used to ensure that the request is not cached by the browser. - Request n°2 (namespace connection request): ``` POST /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 40 ``` Details: ``` 4 => Engine.IO "message" packet type 0 => Socket.IO "CONNECT" packet type ``` - Request n°3 (namespace connection approval) ``` GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 40{"sid":"wZX3oN0bSVIhsaknAAAI"} ``` - Request n°4 `socket.emit('hey', 'Jude')` is executed on the server: ``` GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 42["hey","Jude"] ``` Details: ``` 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type [...] => content ``` - Request n°5 (message out) `socket.emit('hello'); socket.emit('world');` is executed on the client: ``` POST /socket.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC > Content-Type: text/plain; charset=UTF-8 42["hello"]\x1e42["world"] < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 ok ``` Details: ``` 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type ["hello"] => the 1st content \x1e => separator 4 => Engine.IO "message" packet type 2 => Socket.IO "EVENT" packet type ["world"] => the 2nd content ``` - Request n°6 (WebSocket upgrade) ``` GET /socket.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC < HTTP/1.1 101 Switching Protocols ``` WebSocket frames: ``` < 2probe => Engine.IO probe request > 3probe => Engine.IO probe response > 5 => Engine.IO "upgrade" packet type > 42["hello"] > 42["world"] > 40/admin, => request access to the admin namespace (Socket.IO "CONNECT" packet) < 40/admin,{"sid":"-G5j-67EZFp-q59rADQM"} => grant access to the admin namespace > 42/admin,1["tellme"] => Socket.IO "EVENT" packet with acknowledgement < 461-/admin,1[{"_placeholder":true,"num":0}] => Socket.IO "BINARY_ACK" packet with a placeholder < => the binary attachment (sent in the following frame) ... after a while without message > 2 => Engine.IO "ping" packet type < 3 => Engine.IO "pong" packet type > 1 => Engine.IO "close" packet type ``` ## History ### Difference between v5 and v4 The 5th revision (current) of the Socket.IO protocol is used in Socket.IO v3 and above (`v3.0.0` was released in November 2020). It is built on top of the 4th revision of [the Engine.IO protocol](https://github.com/socketio/engine.io-protocol) (hence the `EIO=4` query parameter). List of changes: - remove the implicit connection to the default namespace In previous versions, a client was always connected to the default namespace, even if it requested access to another namespace. This is not the case anymore, the client must send a `CONNECT` packet in any case. Commits: [09b6f23](https://github.com/socketio/socket.io/commit/09b6f2333950b8afc8c1400b504b01ad757876bd) (server) and [249e0be](https://github.com/socketio/socket.io-client/commit/249e0bef9071e7afd785485961c4eef0094254e8) (client) - rename `ERROR` to `CONNECT_ERROR` The meaning and the code number (4) are not modified: this packet type is still used by the server when the connection to a namespace is refused. But we feel the name is more self-descriptive. Commits: [d16c035](https://github.com/socketio/socket.io/commit/d16c035d258b8deb138f71801cb5aeedcdb3f002) (server) and [13e1db7c](https://github.com/socketio/socket.io-client/commit/13e1db7c94291c583d843beaa9e06ee041ae4f26) (client). - the `CONNECT` packet now can contain a payload The client can send a payload for authentication/authorization purposes. Example: ```json { "type": 0, "nsp": "/admin", "data": { "token": "123" } } ``` In case of success, the server responds with a payload contain the ID of the Socket. Example: ```json { "type": 0, "nsp": "/admin", "data": { "sid": "CjdVH4TQvovi1VvgAC5Z" } } ``` This change means that the ID of the Socket.IO connection will now be different from the ID of the underlying Engine.IO connection (the one that is found in the query parameters of the HTTP requests). Commits: [2875d2c](https://github.com/socketio/socket.io/commit/2875d2cfdfa463e64cb520099749f543bbc4eb15) (server) and [bbe94ad](https://github.com/socketio/socket.io-client/commit/bbe94adb822a306c6272e977d394e3e203cae25d) (client) - the payload `CONNECT_ERROR` packet is now an object instead of a plain string Commits: [54bf4a4](https://github.com/socketio/socket.io/commit/54bf4a44e9e896dfb64764ee7bd4e8823eb7dc7b) (server) and [0939395](https://github.com/socketio/socket.io-client/commit/09393952e3397a0c71f239ea983f8ec1623b7c21) (client) ### Difference between v4 and v3 The 4th revision of the Socket.IO protocol is used in Socket.IO v1 (`v1.0.3` was released in June 2014) and v2 (`v2.0.0` was released in May 2017). The details of the revision can be found here: https://github.com/socketio/socket.io-protocol/tree/v4 It is built on top of the 3rd revision of [the Engine.IO protocol](https://github.com/socketio/engine.io-protocol) (hence the `EIO=3` query parameter). List of changes: - add a `BINARY_ACK` packet type Previously, an `ACK` packet was always treated as if it may contain binary objects, with recursive search for such objects, which could hurt performance. Reference: https://github.com/socketio/socket.io-parser/commit/ca4f42a922ba7078e840b1bc09fe3ad618acc065 ### Difference between v3 and v2 The 3rd revision of the Socket.IO protocol is used in early Socket.IO v1 versions (`socket.io@1.0.0...1.0.2`) (released in May 2014). The details of the revision can be found here: https://github.com/socketio/socket.io-protocol/tree/v3 List of changes: - remove the usage of msgpack to encode packets containing binary objects (see also [299849b](https://github.com/socketio/socket.io-parser/commit/299849b00294c3bc95817572441f3aca8ffb1f65)) ### Difference between v2 and v1 List of changes: - add a `BINARY_EVENT` packet type This was added during the work towards Socket.IO 1.0, in order to add support for binary objects. The `BINARY_EVENT` packets were encoded with [msgpack](https://msgpack.org/). ### Initial revision This first revision was the result of the split between the Engine.IO protocol (low-level plumbing with WebSocket / HTTP long-polling, heartbeat) and the Socket.IO protocol. It was never included in a Socket.IO release, but paved the way for the next iterations. ## Test suite The test suite in the [`test-suite/`](https://github.com/socketio/socket.io-protocol/tree/main/test-suite) directory lets you check the compliance of a server implementation. Usage: - in Node.js: `npm ci && npm test` - in a browser: simply open the `index.html` file in your browser For reference, here is expected configuration for the JavaScript server to pass all tests: ```js import { Server } from "socket.io"; const io = new Server(3000, { pingInterval: 300, pingTimeout: 200, maxPayload: 1000000, connectTimeout: 1000, cors: { origin: "*" } }); io.on("connection", (socket) => { socket.emit("auth", socket.handshake.auth); socket.on("message", (...args) => { socket.emit.apply(socket, ["message-back", ...args]); }); socket.on("message-with-ack", (...args) => { const ack = args.pop(); ack(...args); }) }); io.of("/custom").on("connection", (socket) => { socket.emit("auth", socket.handshake.auth); }); ``` ## License MIT ================================================ FILE: docs/socket.io-protocol/v5-test-suite/.gitignore ================================================ node_modules ================================================ FILE: docs/socket.io-protocol/v5-test-suite/index.html ================================================ Test suite for the Socket.IO protocol
================================================ FILE: docs/socket.io-protocol/v5-test-suite/node-imports.js ================================================ import fetch from "node-fetch"; import { WebSocket } from "ws"; import chai from "chai"; import chaiString from "chai-string"; chai.use(chaiString); globalThis.fetch = fetch; globalThis.WebSocket = WebSocket; globalThis.chai = chai; ================================================ FILE: docs/socket.io-protocol/v5-test-suite/package.json ================================================ { "name": "socket.io-protocol-test-suite", "version": "0.0.1", "private": true, "type": "module", "scripts": { "format": "prettier -w *.js", "test": "mocha test-suite.js" }, "devDependencies": { "chai": "^4.3.6", "chai-string": "^1.5.0", "mocha": "^9.2.1", "node-fetch": "^3.2.0", "prettier": "^2.5.1", "ws": "^8.5.0" } } ================================================ FILE: docs/socket.io-protocol/v5-test-suite/test-suite.js ================================================ const isNodejs = typeof window === "undefined"; if (isNodejs) { // make the tests runnable in both the browser and Node.js await import("./node-imports.js"); } const { expect } = chai; const URL = "http://localhost:3000"; const WS_URL = URL.replace("http", "ws"); const PING_INTERVAL = 300; const PING_TIMEOUT = 200; function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } function createWebSocket(url) { const socket = new WebSocket(url); socket._eventBuffer = {}; socket._pendingPromises = {}; for (const eventType of ["open", "close", "message"]) { socket._eventBuffer[eventType] = []; socket._pendingPromises[eventType] = []; socket.addEventListener(eventType, (event) => { if (socket._pendingPromises[eventType].length) { socket._pendingPromises[eventType].shift()(event); } else { socket._eventBuffer[eventType].push(event); } }); } return socket; } function waitFor(socket, eventType) { if (socket._eventBuffer[eventType].length) { return Promise.resolve(socket._eventBuffer[eventType].shift()); } else { return new Promise((resolve) => { socket._pendingPromises[eventType].push(resolve); }); } } function waitForPackets(socket, count) { const packets = []; return new Promise((resolve) => { const handler = (event) => { if (event.data === "2") { // ignore PING packets return; } packets.push(event.data); if (packets.length === count) { socket.removeEventListener("message", handler); resolve(packets); } }; socket.addEventListener("message", handler); }); } async function initLongPollingSession() { const response = await fetch(`${URL}/socket.io/?EIO=4&transport=polling`); const content = await response.text(); return JSON.parse(content.substring(1)).sid; } async function initSocketIOConnection() { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); socket.binaryType = "arraybuffer"; await waitFor(socket, "message"); // Engine.IO handshake socket.send("40"); await waitFor(socket, "message"); // Socket.IO handshake await waitFor(socket, "message"); // "auth" packet return socket; } describe("Engine.IO protocol", () => { describe("handshake", () => { describe("HTTP long-polling", () => { it("should successfully open a session", async () => { const response = await fetch( `${URL}/socket.io/?EIO=4&transport=polling` ); expect(response.status).to.eql(200); const content = await response.text(); expect(content).to.startsWith("0"); const value = JSON.parse(content.substring(1)); expect(value).to.have.all.keys( "sid", "upgrades", "pingInterval", "pingTimeout", "maxPayload" ); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql(["websocket"]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.eql(1000000); }); it("should fail with an invalid 'EIO' query parameter", async () => { const response = await fetch(`${URL}/socket.io/?transport=polling`); expect(response.status).to.eql(400); const response2 = await fetch( `${URL}/socket.io/?EIO=abc&transport=polling` ); expect(response2.status).to.eql(400); }); it("should fail with an invalid 'transport' query parameter", async () => { const response = await fetch(`${URL}/socket.io/?EIO=4`); expect(response.status).to.eql(400); const response2 = await fetch(`${URL}/socket.io/?EIO=4&transport=abc`); expect(response2.status).to.eql(400); }); it("should fail with an invalid request method", async () => { const response = await fetch( `${URL}/socket.io/?EIO=4&transport=polling`, { method: "post", } ); expect(response.status).to.eql(400); const response2 = await fetch( `${URL}/socket.io/?EIO=4&transport=polling`, { method: "put", } ); expect(response2.status).to.eql(400); }); }); describe("WebSocket", () => { it("should successfully open a session", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("0"); const value = JSON.parse(data.substring(1)); expect(value).to.have.all.keys( "sid", "upgrades", "pingInterval", "pingTimeout", "maxPayload" ); expect(value.sid).to.be.a("string"); expect(value.upgrades).to.eql([]); expect(value.pingInterval).to.eql(PING_INTERVAL); expect(value.pingTimeout).to.eql(PING_TIMEOUT); expect(value.maxPayload).to.eql(1000000); socket.close(); }); it("should fail with an invalid 'EIO' query parameter", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?transport=websocket` ); if (isNodejs) { socket.on("error", () => {}); } await waitFor(socket, "close"); const socket2 = createWebSocket( `${WS_URL}/socket.io/?EIO=abc&transport=websocket` ); if (isNodejs) { socket2.on("error", () => {}); } await waitFor(socket2, "close"); }); it("should fail with an invalid 'transport' query parameter", async () => { const socket = createWebSocket(`${WS_URL}/socket.io/?EIO=4`); if (isNodejs) { socket.on("error", () => {}); } await waitFor(socket, "close"); const socket2 = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=abc` ); if (isNodejs) { socket2.on("error", () => {}); } await waitFor(socket2, "close"); }); }); }); describe("heartbeat", function () { this.timeout(5000); describe("HTTP long-polling", () => { it("should send ping/pong packets", async () => { const sid = await initLongPollingSession(); for (let i = 0; i < 3; i++) { const pollResponse = await fetch( `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(200); const pollContent = await pollResponse.text(); expect(pollContent).to.eql("2"); const pushResponse = await fetch( `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "3", } ); expect(pushResponse.status).to.eql(200); } }); it("should close the session upon ping timeout", async () => { const sid = await initLongPollingSession(); await sleep(PING_INTERVAL + PING_TIMEOUT); const pollResponse = await fetch( `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); }); describe("WebSocket", () => { it("should send ping/pong packets", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // handshake for (let i = 0; i < 3; i++) { const { data } = await waitFor(socket, "message"); expect(data).to.eql("2"); socket.send("3"); } socket.close(); }); it("should close the session upon ping timeout", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "close"); // handshake }); }); }); describe("close", () => { describe("HTTP long-polling", () => { it("should forcefully close the session", async () => { const sid = await initLongPollingSession(); const [pollResponse] = await Promise.all([ fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`), fetch(`${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}`, { method: "post", body: "1", }), ]); expect(pollResponse.status).to.eql(200); const pullContent = await pollResponse.text(); expect(pullContent).to.eql("6"); const pollResponse2 = await fetch( `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse2.status).to.eql(400); }); }); describe("WebSocket", () => { it("should forcefully close the session", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // handshake socket.send("1"); await waitFor(socket, "close"); }); }); }); describe("upgrade", () => { it("should successfully upgrade from HTTP long-polling to WebSocket", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); // send probe socket.send("2probe"); const probeResponse = await waitFor(socket, "message"); expect(probeResponse.data).to.eql("3probe"); // complete upgrade socket.send("5"); }); it("should ignore HTTP requests with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); await waitFor(socket, "message"); // "3probe" socket.send("5"); const pollResponse = await fetch( `${URL}/socket.io/?EIO=4&transport=polling&sid=${sid}` ); expect(pollResponse.status).to.eql(400); }); it("should ignore WebSocket connection with same sid after upgrade", async () => { const sid = await initLongPollingSession(); const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket, "open"); socket.send("2probe"); await waitFor(socket, "message"); // "3probe" socket.send("5"); const socket2 = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket&sid=${sid}` ); await waitFor(socket2, "close"); }); }); }); describe("Socket.IO protocol", () => { describe("connect", () => { it("should allow connection to the main namespace", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send("40"); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("40"); const handshake = JSON.parse(data.substring(2)); expect(handshake).to.have.all.keys("sid"); expect(handshake.sid).to.be.a("string"); const authPacket = await waitFor(socket, "message"); expect(authPacket.data).to.eql('42["auth",{}]'); }); it("should allow connection to the main namespace with a payload", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send('40{"token":"123"}'); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("40"); const handshake = JSON.parse(data.substring(2)); expect(handshake).to.have.all.keys("sid"); expect(handshake.sid).to.be.a("string"); const authPacket = await waitFor(socket, "message"); expect(authPacket.data).to.eql('42["auth",{"token":"123"}]'); }); it("should allow connection to a custom namespace", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send("40/custom,"); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("40/custom,"); const handshake = JSON.parse(data.substring(10)); expect(handshake).to.have.all.keys("sid"); expect(handshake.sid).to.be.a("string"); const authPacket = await waitFor(socket, "message"); expect(authPacket.data).to.eql('42/custom,["auth",{}]'); }); it("should allow connection to a custom namespace with a payload", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send('40/custom,{"token":"abc"}'); const { data } = await waitFor(socket, "message"); expect(data).to.startsWith("40/custom,"); const handshake = JSON.parse(data.substring(10)); expect(handshake).to.have.all.keys("sid"); expect(handshake.sid).to.be.a("string"); const authPacket = await waitFor(socket, "message"); expect(authPacket.data).to.eql('42/custom,["auth",{"token":"abc"}]'); }); it("should disallow connection to an unknown namespace", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send("40/random"); const { data } = await waitFor(socket, "message"); expect(data).to.eql('44/random,{"message":"Invalid namespace"}'); }); it("should disallow connection with an invalid handshake", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "message"); // Engine.IO handshake socket.send("4abc"); await waitFor(socket, "close"); }); it("should close the connection if no handshake is received", async () => { const socket = createWebSocket( `${WS_URL}/socket.io/?EIO=4&transport=websocket` ); await waitFor(socket, "close"); }); }); describe("disconnect", () => { it("should disconnect from the main namespace", async () => { const socket = await initSocketIOConnection(); socket.send("41"); const { data } = await waitFor(socket, "message"); expect(data).to.eql("2"); }); it("should connect then disconnect from a custom namespace", async () => { const socket = await initSocketIOConnection(); await waitFor(socket, "message"); // ping socket.send("40/custom"); await waitFor(socket, "message"); // Socket.IO handshake await waitFor(socket, "message"); // auth packet socket.send("41/custom"); socket.send('42["message","message to main namespace"]'); const { data } = await waitFor(socket, "message"); expect(data).to.eql('42["message-back","message to main namespace"]'); }); }); describe("message", () => { it("should send a plain-text packet", async () => { const socket = await initSocketIOConnection(); socket.send('42["message",1,"2",{"3":[true]}]'); const { data } = await waitFor(socket, "message"); expect(data).to.eql('42["message-back",1,"2",{"3":[true]}]'); }); it("should send a packet with binary attachments", async () => { const socket = await initSocketIOConnection(); socket.send( '452-["message",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); socket.send(Uint8Array.from([1, 2, 3])); socket.send(Uint8Array.from([4, 5, 6])); const packets = await waitForPackets(socket, 3); expect(packets[0]).to.eql( '452-["message-back",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); socket.close(); }); it("should send a plain-text packet with an ack", async () => { const socket = await initSocketIOConnection(); socket.send('42456["message-with-ack",1,"2",{"3":[false]}]'); const { data } = await waitFor(socket, "message"); expect(data).to.eql('43456[1,"2",{"3":[false]}]'); }); it("should send a packet with binary attachments and an ack", async () => { const socket = await initSocketIOConnection(); socket.send( '452-789["message-with-ack",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); socket.send(Uint8Array.from([1, 2, 3])); socket.send(Uint8Array.from([4, 5, 6])); const packets = await waitForPackets(socket, 3); expect(packets[0]).to.eql( '462-789[{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]' ); expect(packets[1]).to.eql(Uint8Array.from([1, 2, 3]).buffer); expect(packets[2]).to.eql(Uint8Array.from([4, 5, 6]).buffer); socket.close(); }); it("should close the connection upon invalid format (unknown packet type)", async () => { const socket = await initSocketIOConnection(); socket.send("4abc"); await waitFor(socket, "close"); }); it("should close the connection upon invalid format (invalid payload format)", async () => { const socket = await initSocketIOConnection(); socket.send("42{}"); await waitFor(socket, "close"); }); it("should close the connection upon invalid format (invalid ack id)", async () => { const socket = await initSocketIOConnection(); socket.send('42abc["message-with-ack",1,"2",{"3":[false]}]'); await waitFor(socket, "close"); }); }); }); ================================================ FILE: examples/.gitignore ================================================ package-lock.json ================================================ FILE: examples/ReactNativeExample/.bundle/config ================================================ BUNDLE_PATH: "vendor/bundle" BUNDLE_FORCE_RUBY_PLATFORM: 1 ================================================ FILE: examples/ReactNativeExample/.eslintrc.js ================================================ module.exports = { root: true, extends: '@react-native', }; ================================================ FILE: examples/ReactNativeExample/.gitignore ================================================ # OSX # .DS_Store # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate ios/.xcode.env.local # Android/IntelliJ # build/ .idea .gradle local.properties *.iml *.hprof .cxx/ *.keystore !debug.keystore # node.js # node_modules/ npm-debug.log yarn-error.log # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/ **/fastlane/report.xml **/fastlane/Preview.html **/fastlane/screenshots **/fastlane/test_output # Bundle artifact *.jsbundle # Ruby / CocoaPods /ios/Pods/ /vendor/bundle/ # Temporary files created by Metro to check the health of the file watcher .metro-health-check* # testing /coverage ================================================ FILE: examples/ReactNativeExample/.prettierrc.js ================================================ module.exports = { arrowParens: 'avoid', bracketSameLine: true, bracketSpacing: false, singleQuote: true, trailingComma: 'all', }; ================================================ FILE: examples/ReactNativeExample/.watchmanconfig ================================================ {} ================================================ FILE: examples/ReactNativeExample/App.tsx ================================================ /** * Sample React Native App * https://github.com/facebook/react-native * * @format */ import React from 'react'; import type {PropsWithChildren} from 'react'; import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, useColorScheme, View, } from 'react-native'; import { socket } from './socket'; import { useEffect, useState } from 'react'; import { Colors, DebugInstructions, Header, LearnMoreLinks, ReloadInstructions, } from 'react-native/Libraries/NewAppScreen'; type SectionProps = PropsWithChildren<{ title: string; }>; function Section({children, title}: SectionProps): React.JSX.Element { const isDarkMode = useColorScheme() === 'dark'; return ( {title} {children} ); } function App(): React.JSX.Element { const [isConnected, setIsConnected] = useState(false); const [transport, setTransport] = useState('N/A'); useEffect(() => { if (socket.connected) { onConnect(); } function onConnect() { setIsConnected(true); setTransport(socket.io.engine.transport.name); socket.io.engine.on('upgrade', (transport) => { setTransport(transport.name); }); } function onDisconnect() { setIsConnected(false); setTransport('N/A'); } socket.on('connect', onConnect); socket.on('disconnect', onDisconnect); return () => { socket.off('connect', onConnect); socket.off('disconnect', onDisconnect); }; }, []); return ( Status: { isConnected ? 'connected' : 'disconnected' } Transport: { transport } ); } const styles = StyleSheet.create({ sectionContainer: { marginTop: 32, paddingHorizontal: 24, }, sectionTitle: { fontSize: 24, fontWeight: '600', }, sectionDescription: { marginTop: 8, fontSize: 18, fontWeight: '400', }, highlight: { fontWeight: '700', }, }); export default App; ================================================ FILE: examples/ReactNativeExample/Gemfile ================================================ source 'https://rubygems.org' # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby ">= 2.6.10" # Cocoapods 1.15 introduced a bug which break the build. We will remove the upper # bound in the template on Cocoapods with next React Native release. gem 'cocoapods', '>= 1.13', '< 1.15' gem 'activesupport', '>= 6.1.7.5', '< 7.1.0' ================================================ FILE: examples/ReactNativeExample/README.md ================================================ This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). # Getting Started >**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. ## Step 1: Start the Metro Server First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. To start Metro, run the following command from the _root_ of your React Native project: ```bash # using npm npm start # OR using Yarn yarn start ``` ## Step 2: Start your Application Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: ### For Android ```bash # using npm npm run android # OR using Yarn yarn android ``` ### For iOS ```bash # using npm npm run ios # OR using Yarn yarn ios ``` If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. ## Step 3: Start the Socket.IO server ```bash cd server npm install npm start ``` ## Step 4: Modifying your App Now that you have successfully run the app, let's modify it. 1. Open `App.tsx` in your text editor of choice and edit some lines. 2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! ## Congratulations! :tada: You've successfully run and modified your React Native App. :partying_face: ### Now what? - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). - If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started). # Troubleshooting If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. # Learn More To learn more about React Native, take a look at the following resources: - [React Native Website](https://reactnative.dev) - learn more about React Native. - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. ================================================ FILE: examples/ReactNativeExample/__tests__/App.test.tsx ================================================ /** * @format */ import 'react-native'; import React from 'react'; import App from '../App'; // Note: import explicitly to use the types shipped with jest. import {it} from '@jest/globals'; // Note: test renderer must be required after react-native. import renderer from 'react-test-renderer'; it('renders correctly', () => { renderer.create(); }); ================================================ FILE: examples/ReactNativeExample/android/app/build.gradle ================================================ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. */ react { /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native // reactNativeDir = file("../node_modules/react-native") // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen // codegenDir = file("../node_modules/@react-native/codegen") // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js // cliFile = file("../node_modules/react-native/cli.js") /* Variants */ // The list of variants to that are debuggable. For those we're going to // skip the bundling of the JS bundle and the assets. By default is just 'debug'. // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. // debuggableVariants = ["liteDebug", "prodDebug"] /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] // // The command to run when bundling. By default is 'bundle' // bundleCommand = "ram-bundle" // // The path to the CLI configuration file. Default is empty. // bundleConfig = file(../rn-cli.config.js) // // The name of the generated asset file containing your JS bundle // bundleAssetName = "MyApplication.android.bundle" // // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' // entryFile = file("../js/MyApplication.android.js") // // A list of extra flags to pass to the 'bundle' commands. // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle // extraPackagerArgs = [] /* Hermes Commands */ // The hermes compiler command to run. By default it is 'hermesc' // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" // // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] } /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ def enableProguardInReleaseBuilds = false /** * The preferred build flavor of JavaScriptCore (JSC) * * For example, to use the international variant, you can use: * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ def jscFlavor = 'org.webkit:android-jsc:+' android { ndkVersion rootProject.ext.ndkVersion buildToolsVersion rootProject.ext.buildToolsVersion compileSdk rootProject.ext.compileSdkVersion namespace "com.reactnativeexample" defaultConfig { applicationId "com.reactnativeexample" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } signingConfigs { debug { storeFile file('debug.keystore') storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' } } buildTypes { debug { signingConfig signingConfigs.debug } release { // Caution! In production, you need to generate your own keystore file. // see https://reactnative.dev/docs/signed-apk-android. signingConfig signingConfigs.debug minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } } dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation("com.facebook.react:flipper-integration") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") } else { implementation jscFlavor } } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) ================================================ FILE: examples/ReactNativeExample/android/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: ================================================ FILE: examples/ReactNativeExample/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: examples/ReactNativeExample/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainActivity.kt ================================================ package com.reactnativeexample import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate class MainActivity : ReactActivity() { /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. */ override fun getMainComponentName(): String = "ReactNativeExample" /** * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] */ override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) } ================================================ FILE: examples/ReactNativeExample/android/app/src/main/java/com/reactnativeexample/MainApplication.kt ================================================ package com.reactnativeexample import android.app.Application import com.facebook.react.PackageList import com.facebook.react.ReactApplication import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.flipper.ReactNativeFlipper import com.facebook.soloader.SoLoader class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { override fun getPackages(): List = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) } override fun getJSMainModuleName(): String = "index" override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED } override val reactHost: ReactHost get() = getDefaultReactHost(this.applicationContext, reactNativeHost) override fun onCreate() { super.onCreate() SoLoader.init(this, false) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) } } ================================================ FILE: examples/ReactNativeExample/android/app/src/main/res/drawable/rn_edit_text_material.xml ================================================ ================================================ FILE: examples/ReactNativeExample/android/app/src/main/res/values/strings.xml ================================================ ReactNativeExample ================================================ FILE: examples/ReactNativeExample/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: examples/ReactNativeExample/android/build.gradle ================================================ buildscript { ext { buildToolsVersion = "34.0.0" minSdkVersion = 21 compileSdkVersion = 34 targetSdkVersion = 34 ndkVersion = "25.1.8937393" kotlinVersion = "1.8.0" } repositories { google() mavenCentral() } dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") } } apply plugin: "com.facebook.react.rootproject" ================================================ FILE: examples/ReactNativeExample/android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: examples/ReactNativeExample/android/gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # Use this property to enable support to the new architecture. # This will allow you to use TurboModules and the Fabric render in # your application. You should enable this flag either if you want # to write custom TurboModules/Fabric components OR use libraries that # are providing them. newArchEnabled=false # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. hermesEnabled=true ================================================ FILE: examples/ReactNativeExample/android/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: examples/ReactNativeExample/android/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: examples/ReactNativeExample/android/settings.gradle ================================================ rootProject.name = 'ReactNativeExample' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') ================================================ FILE: examples/ReactNativeExample/app.json ================================================ { "name": "ReactNativeExample", "displayName": "ReactNativeExample" } ================================================ FILE: examples/ReactNativeExample/babel.config.js ================================================ module.exports = { presets: ['module:@react-native/babel-preset'], }; ================================================ FILE: examples/ReactNativeExample/index.js ================================================ /** * @format */ import {AppRegistry} from 'react-native'; import App from './App'; import {name as appName} from './app.json'; AppRegistry.registerComponent(appName, () => App); ================================================ FILE: examples/ReactNativeExample/ios/.xcode.env ================================================ # This `.xcode.env` file is versioned and is used to source the environment # used when running script phases inside Xcode. # To customize your local environment, you can create an `.xcode.env.local` # file that is not versioned. # NODE_BINARY variable contains the PATH to the node executable. # # Customize the NODE_BINARY variable here. # For example, to use nvm with brew, add the following line # . "$(brew --prefix nvm)/nvm.sh" --no-use export NODE_BINARY=$(command -v node) ================================================ FILE: examples/ReactNativeExample/ios/Podfile ================================================ # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native/scripts/react_native_pods.rb", {paths: [process.argv[1]]}, )', __dir__]).strip platform :ios, min_ios_version_supported prepare_react_native_project! # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` # ```js # module.exports = { # dependencies: { # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), # ``` flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym end target 'ReactNativeExample' do config = use_native_modules! use_react_native!( :path => config[:reactNativePath], # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) target 'ReactNativeExampleTests' do inherit! :complete # Pods for testing end post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) end end ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.h ================================================ #import #import @interface AppDelegate : RCTAppDelegate @end ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/AppDelegate.mm ================================================ #import "AppDelegate.h" #import @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"ReactNativeExample"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { return [self getBundleURL]; } - (NSURL *)getBundleURL { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } @end ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/Images.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName ReactNativeExample CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsLocalNetworking NSLocationWhenInUseUsageDescription UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/LaunchScreen.storyboard ================================================ ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample/main.m ================================================ #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeExampleTests.m */; }; 0C80B921A6F3F58F76C31292 /* libPods-ReactNativeExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeExample.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-ReactNativeExample-ReactNativeExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeExample-ReactNativeExampleTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; proxyType = 1; remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = ReactNativeExample; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 00E356EE1AD99517003FC87E /* ReactNativeExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactNativeExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* ReactNativeExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeExampleTests.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReactNativeExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ReactNativeExample/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = ReactNativeExample/AppDelegate.mm; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ReactNativeExample/main.m; sourceTree = ""; }; 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeExample-ReactNativeExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeExample-ReactNativeExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B4392A12AC88292D35C810B /* Pods-ReactNativeExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-ReactNativeExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig"; sourceTree = ""; }; 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeExample/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 00E356EB1AD99517003FC87E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7699B88040F8A987B510C191 /* libPods-ReactNativeExample-ReactNativeExampleTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 0C80B921A6F3F58F76C31292 /* libPods-ReactNativeExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 00E356EF1AD99517003FC87E /* ReactNativeExampleTests */ = { isa = PBXGroup; children = ( 00E356F21AD99517003FC87E /* ReactNativeExampleTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = ReactNativeExampleTests; sourceTree = ""; }; 00E356F01AD99517003FC87E /* Supporting Files */ = { isa = PBXGroup; children = ( 00E356F11AD99517003FC87E /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 13B07FAE1A68108700A75B9A /* ReactNativeExample */ = { isa = PBXGroup; children = ( 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, ); name = ReactNativeExample; sourceTree = ""; }; 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeExample.a */, 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeExample-ReactNativeExampleTests.a */, ); name = Frameworks; sourceTree = ""; }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( ); name = Libraries; sourceTree = ""; }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( 13B07FAE1A68108700A75B9A /* ReactNativeExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* ReactNativeExampleTests */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, BBD78D7AC51CEA395F1C20DB /* Pods */, ); indentWidth = 2; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* ReactNativeExample.app */, 00E356EE1AD99517003FC87E /* ReactNativeExampleTests.xctest */, ); name = Products; sourceTree = ""; }; BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( 3B4392A12AC88292D35C810B /* Pods-ReactNativeExample.debug.xcconfig */, 5709B34CF0A7D63546082F79 /* Pods-ReactNativeExample.release.xcconfig */, 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig */, 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 00E356ED1AD99517003FC87E /* ReactNativeExampleTests */ = { isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ReactNativeExampleTests" */; buildPhases = ( A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */, F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( 00E356F51AD99517003FC87E /* PBXTargetDependency */, ); name = ReactNativeExampleTests; productName = ReactNativeExampleTests; productReference = 00E356EE1AD99517003FC87E /* ReactNativeExampleTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 13B07F861A680F5B00A75B9A /* ReactNativeExample */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeExample" */; buildPhases = ( C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = ReactNativeExample; productName = ReactNativeExample; productReference = 13B07F961A680F5B00A75B9A /* ReactNativeExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1210; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1120; }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeExample" */; compatibilityVersion = "Xcode 12.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* ReactNativeExample */, 00E356ED1AD99517003FC87E /* ReactNativeExampleTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 00E356EC1AD99517003FC87E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "$(SRCROOT)/.xcode.env.local", "$(SRCROOT)/.xcode.env", ); name = "Bundle React Native code and images"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-ReactNativeExample-ReactNativeExampleTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-ReactNativeExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample/Pods-ReactNativeExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeExample-ReactNativeExampleTests/Pods-ReactNativeExample-ReactNativeExampleTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 00E356EA1AD99517003FC87E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 00E356F31AD99517003FC87E /* ReactNativeExampleTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 13B07F861A680F5B00A75B9A /* ReactNativeExample */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeExample-ReactNativeExampleTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = ReactNativeExampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReactNativeExample.app/ReactNativeExample"; }; name = Debug; }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeExample-ReactNativeExampleTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; INFOPLIST_FILE = ReactNativeExampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReactNativeExample.app/ReactNativeExample"; }; name = Release; }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-ReactNativeExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; INFOPLIST_FILE = ReactNativeExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = ReactNativeExample; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-ReactNativeExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = ReactNativeExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = ReactNativeExample; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", ); LIBRARY_SEARCH_PATHS = ( "\"$(SDKROOT)/usr/lib/swift\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); SDKROOT = iphoneos; }; name = Debug; }; 83CBBA211A601CBA00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", ); LIBRARY_SEARCH_PATHS = ( "\"$(SDKROOT)/usr/lib/swift\"", "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ReactNativeExampleTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 00E356F61AD99517003FC87E /* Debug */, 00E356F71AD99517003FC87E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 13B07F941A680F5B00A75B9A /* Debug */, 13B07F951A680F5B00A75B9A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA211A601CBA00E9B192 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExample.xcodeproj/xcshareddata/xcschemes/ReactNativeExample.xcscheme ================================================ ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExampleTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: examples/ReactNativeExample/ios/ReactNativeExampleTests/ReactNativeExampleTests.m ================================================ #import #import #import #import #define TIMEOUT_SECONDS 600 #define TEXT_TO_LOOK_FOR @"Welcome to React" @interface ReactNativeExampleTests : XCTestCase @end @implementation ReactNativeExampleTests - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test { if (test(view)) { return YES; } for (UIView *subview in [view subviews]) { if ([self findSubviewInView:subview matching:test]) { return YES; } } return NO; } - (void)testRendersWelcomeScreen { UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; BOOL foundElement = NO; __block NSString *redboxError = nil; #ifdef DEBUG RCTSetLogFunction( ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { if (level >= RCTLogLevelError) { redboxError = message; } }); #endif while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { return YES; } return NO; }]; } #ifdef DEBUG RCTSetLogFunction(RCTDefaultLogFunction); #endif XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); } @end ================================================ FILE: examples/ReactNativeExample/jest.config.js ================================================ module.exports = { preset: 'react-native', }; ================================================ FILE: examples/ReactNativeExample/metro.config.js ================================================ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); /** * Metro configuration * https://facebook.github.io/metro/docs/configuration * * @type {import('metro-config').MetroConfig} */ const config = {}; module.exports = mergeConfig(getDefaultConfig(__dirname), config); ================================================ FILE: examples/ReactNativeExample/package.json ================================================ { "name": "ReactNativeExample", "version": "0.0.1", "private": true, "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", "lint": "eslint .", "start": "react-native start", "test": "jest" }, "dependencies": { "react": "18.2.0", "react-native": "0.73.6", "socket.io-client": "^4.7.5" }, "devDependencies": { "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@react-native/babel-preset": "0.73.21", "@react-native/eslint-config": "0.73.2", "@react-native/metro-config": "0.73.5", "@react-native/typescript-config": "0.73.1", "@types/react": "^18.2.6", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", "react-test-renderer": "18.2.0", "typescript": "5.0.4" }, "engines": { "node": ">=18" } } ================================================ FILE: examples/ReactNativeExample/server/index.js ================================================ import { Server } from 'socket.io'; const io = new Server(); io.on('connection', (socket) => { console.log(`connect: ${socket.id}`, socket.request.headers); socket.on('disconnect', () => { console.log(`disconnect: ${socket.id}`); }); }); io.listen(3000); ================================================ FILE: examples/ReactNativeExample/server/package.json ================================================ { "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "start": "node index.js" }, "author": "", "license": "ISC", "dependencies": { "socket.io": "^4.7.5" } } ================================================ FILE: examples/ReactNativeExample/socket.js ================================================ import { io } from 'socket.io-client'; export const socket = io('http://192.168.0.10:3000'); // use the IP address of your machine ================================================ FILE: examples/ReactNativeExample/tsconfig.json ================================================ { "extends": "@react-native/typescript-config/tsconfig.json" } ================================================ FILE: examples/angular-todomvc/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. ================================================ FILE: examples/angular-todomvc/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: examples/angular-todomvc/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist /tmp /out-tsc # Only exists if Bazel was run /bazel-out # dependencies /node_modules # profiling files chrome-profiler-events*.json speed-measure-plugin*.json # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # misc /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log yarn-error.log testem.log /typings # System Files .DS_Store Thumbs.db ================================================ FILE: examples/angular-todomvc/README.md ================================================ # Angular TodoMVC + Socket.IO This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4. Inspired from the [TodoMVC](http://todomvc.com/) [angular example](https://github.com/tastejs/todomvc/tree/master/examples/angular2). ![demo](assets/demo.gif) ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ## Socket.IO server Run `npm run start:server` to start the Socket.IO server. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular-todomvc/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "angular-todomvc": { "projectType": "application", "schematics": { "@schematics/angular:application": { "strict": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/angular-todomvc", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "aot": true, "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] }, "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ] } } }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "angular-todomvc:build" }, "configurations": { "production": { "browserTarget": "angular-todomvc:build:production" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "angular-todomvc:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ "tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { "protractorConfig": "e2e/protractor.conf.js", "devServerTarget": "angular-todomvc:serve" }, "configurations": { "production": { "devServerTarget": "angular-todomvc:serve:production" } } } } } }, "defaultProject": "angular-todomvc" } ================================================ FILE: examples/angular-todomvc/e2e/protractor.conf.js ================================================ // @ts-check // Protractor configuration file, see link for more information // https://github.com/angular/protractor/blob/master/lib/config.ts const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); /** * @type { import("protractor").Config } */ exports.config = { allScriptsTimeout: 11000, specs: [ './src/**/*.e2e-spec.ts' ], capabilities: { browserName: 'chrome' }, directConnect: true, SELENIUM_PROMISE_MANAGER: false, baseUrl: 'http://localhost:4200/', framework: 'jasmine', jasmineNodeOpts: { showColors: true, defaultTimeoutInterval: 30000, print: function() {} }, onPrepare() { require('ts-node').register({ project: require('path').join(__dirname, './tsconfig.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: StacktraceOption.PRETTY } })); } }; ================================================ FILE: examples/angular-todomvc/e2e/src/app.e2e-spec.ts ================================================ import { AppPage } from './app.po'; import { browser, logging } from 'protractor'; describe('workspace-project App', () => { let page: AppPage; beforeEach(() => { page = new AppPage(); }); it('should display welcome message', async () => { await page.navigateTo(); expect(await page.getTitleText()).toEqual('angular-todomvc app is running!'); }); afterEach(async () => { // Assert that there are no errors emitted from the browser const logs = await browser.manage().logs().get(logging.Type.BROWSER); expect(logs).not.toContain(jasmine.objectContaining({ level: logging.Level.SEVERE, } as logging.Entry)); }); }); ================================================ FILE: examples/angular-todomvc/e2e/src/app.po.ts ================================================ import { browser, by, element } from 'protractor'; export class AppPage { async navigateTo(): Promise { return browser.get(browser.baseUrl); } async getTitleText(): Promise { return element(by.css('app-root .content span')).getText(); } } ================================================ FILE: examples/angular-todomvc/e2e/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/e2e", "module": "commonjs", "target": "es2018", "types": [ "jasmine", "node" ] } } ================================================ FILE: examples/angular-todomvc/karma.conf.js ================================================ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/angular-todomvc'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true }); }; ================================================ FILE: examples/angular-todomvc/package.json ================================================ { "name": "angular-todomvc", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "start:server": "ts-node -O '{\"module\":\"commonjs\"}' server.ts" }, "private": true, "dependencies": { "@angular/animations": "~11.0.4", "@angular/common": "~11.0.4", "@angular/compiler": "~11.0.4", "@angular/core": "~11.0.4", "@angular/forms": "~11.0.4", "@angular/platform-browser": "~11.0.4", "@angular/platform-browser-dynamic": "~11.0.4", "@angular/router": "~11.0.4", "rxjs": "~6.6.0", "socket.io": "^4.0.0", "socket.io-client": "^4.0.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" }, "devDependencies": { "@angular-devkit/build-angular": "~0.1100.4", "@angular/cli": "~11.0.4", "@angular/compiler-cli": "~11.0.4", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", "codelyzer": "^6.0.0", "jasmine-core": "~3.6.0", "jasmine-spec-reporter": "~5.0.0", "karma": "~5.1.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "protractor": "~7.0.0", "ts-node": "~8.3.0", "tslint": "~6.1.0", "typescript": "~4.0.2" } } ================================================ FILE: examples/angular-todomvc/server.ts ================================================ import { Server } from "socket.io"; const io = new Server(8080, { cors: { origin: "http://localhost:4200", methods: ["GET", "POST"] } }); interface Todo { completed: boolean; editing: boolean; title: string; } let todos: Array = []; io.on("connection", (socket) => { socket.emit("todos", todos); // note: we could also create a CRUD (create/read/update/delete) service for the todo list socket.on("update-store", (updatedTodos) => { // store it locally todos = updatedTodos; // broadcast to everyone but the sender socket.broadcast.emit("todos", todos); }); }); ================================================ FILE: examples/angular-todomvc/src/app/app.component.css ================================================ ================================================ FILE: examples/angular-todomvc/src/app/app.component.html ================================================

todos

{{todoStore.getRemaining().length}} {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left
================================================ FILE: examples/angular-todomvc/src/app/app.component.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ AppComponent ], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it(`should have as title 'angular-todomvc'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.title).toEqual('angular-todomvc'); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement; expect(compiled.querySelector('.content span').textContent).toContain('angular-todomvc app is running!'); }); }); ================================================ FILE: examples/angular-todomvc/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { RemoteTodoStore, Todo } from './store'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { todoStore: RemoteTodoStore; newTodoText = ''; constructor(todoStore: RemoteTodoStore) { this.todoStore = todoStore; } stopEditing(todo: Todo, editedTitle: string) { todo.title = editedTitle; todo.editing = false; } cancelEditingTodo(todo: Todo) { todo.editing = false; } updateEditingTodo(todo: Todo, editedTitle: string) { editedTitle = editedTitle.trim(); todo.editing = false; if (editedTitle.length === 0) { return this.todoStore.remove(todo); } todo.title = editedTitle; } editTodo(todo: Todo) { todo.editing = true; } removeCompleted() { this.todoStore.removeCompleted(); } toggleCompletion(todo: Todo) { this.todoStore.toggleCompletion(todo); } remove(todo: Todo){ this.todoStore.remove(todo); } addTodo() { if (this.newTodoText.trim().length) { this.todoStore.add(this.newTodoText); this.newTodoText = ''; } } } ================================================ FILE: examples/angular-todomvc/src/app/app.module.ts ================================================ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { RemoteTodoStore } from './store'; import { FormsModule } from "@angular/forms"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule ], providers: [RemoteTodoStore], bootstrap: [AppComponent] }) export class AppModule { } ================================================ FILE: examples/angular-todomvc/src/app/store.ts ================================================ import { io, Socket } from "socket.io-client"; export class Todo { completed: boolean; editing: boolean; private _title: String = ""; get title() { return this._title; } set title(value: String) { this._title = value.trim(); } constructor(title: String) { this.completed = false; this.editing = false; this.title = title.trim(); } } export class TodoStore { todos: Array; constructor() { let persistedTodos = JSON.parse(localStorage.getItem('angular2-todos') || '[]'); // Normalize back into classes this.todos = persistedTodos.map( (todo: {_title: String, completed: boolean}) => { let ret = new Todo(todo._title); ret.completed = todo.completed; return ret; }); } protected updateStore() { localStorage.setItem('angular2-todos', JSON.stringify(this.todos)); } private getWithCompleted(completed: boolean) { return this.todos.filter((todo: Todo) => todo.completed === completed); } allCompleted() { return this.todos.length === this.getCompleted().length; } setAllTo(completed: boolean) { this.todos.forEach((t: Todo) => t.completed = completed); this.updateStore(); } removeCompleted() { this.todos = this.getRemaining(); this.updateStore(); } getRemaining() { return this.getWithCompleted(false); } getCompleted() { return this.getWithCompleted(true); } toggleCompletion(todo: Todo) { todo.completed = !todo.completed; this.updateStore(); } remove(todo: Todo) { this.todos.splice(this.todos.indexOf(todo), 1); this.updateStore(); } add(title: String) { this.todos.push(new Todo(title)); this.updateStore(); } } export class RemoteTodoStore extends TodoStore { private socket: Socket; constructor() { super(); this.socket = io("http://localhost:8080"); this.socket.on("todos", (updatedTodos: Array) => { this.todos = updatedTodos; }); } protected updateStore() { this.socket.emit("update-store", this.todos.map(({ title, editing, completed }) => ({ title, editing, completed }))); } } ================================================ FILE: examples/angular-todomvc/src/assets/.gitkeep ================================================ ================================================ FILE: examples/angular-todomvc/src/environments/environment.prod.ts ================================================ export const environment = { production: true }; ================================================ FILE: examples/angular-todomvc/src/environments/environment.ts ================================================ // This file can be replaced during build by using the `fileReplacements` array. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { production: false }; /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. * * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ // import 'zone.js/dist/zone-error'; // Included with Angular CLI. ================================================ FILE: examples/angular-todomvc/src/index.html ================================================ Angular Todo MVC ================================================ FILE: examples/angular-todomvc/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err)); ================================================ FILE: examples/angular-todomvc/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. /** * Web Animations `@angular/platform-browser/animations` * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). */ // import 'web-animations-js'; // Run `npm install --save web-animations-js`. /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ ================================================ FILE: examples/angular-todomvc/src/styles.css ================================================ /* imported from node_modules/todomvc-app-css/index.css */ html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #111111; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp h1 { position: absolute; top: -140px; width: 100%; font-size: 80px; font-weight: 200; text-align: center; color: #b83f45; -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { width: 60px; height: 34px; font-size: 0; position: absolute; top: -52px; left: -13px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all + label:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked + label:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; font-weight: 400; color: #4d4d4d; } .todo-list li.completed label { color: #cdcdcd; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { padding: 10px 15px; height: 20px; text-align: center; font-size: 15px; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #4d4d4d; font-size: 11px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ================================================ FILE: examples/angular-todomvc/src/test.ts ================================================ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/dist/zone-testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { keys(): string[]; (id: string): T; }; }; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); ================================================ FILE: examples/angular-todomvc/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: examples/angular-todomvc/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "es2015", "module": "es2020", "lib": [ "es2018", "dom" ] }, "angularCompilerOptions": { "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular-todomvc/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "files": [ "src/test.ts", "src/polyfills.ts" ], "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: examples/angular-todomvc/tslint.json ================================================ { "extends": "tslint:recommended", "rulesDirectory": [ "codelyzer" ], "rules": { "align": { "options": [ "parameters", "statements" ] }, "array-type": false, "arrow-return-shorthand": true, "curly": true, "deprecation": { "severity": "warning" }, "eofline": true, "import-blacklist": [ true, "rxjs/Rx" ], "import-spacing": true, "indent": { "options": [ "spaces" ] }, "max-classes-per-file": false, "max-line-length": [ true, 140 ], "member-ordering": [ true, { "order": [ "static-field", "instance-field", "static-method", "instance-method" ] } ], "no-console": [ true, "debug", "info", "time", "timeEnd", "trace" ], "no-empty": false, "no-inferrable-types": [ true, "ignore-params" ], "no-non-null-assertion": true, "no-redundant-jsdoc": true, "no-switch-case-fall-through": true, "no-var-requires": false, "object-literal-key-quotes": [ true, "as-needed" ], "quotemark": [ true, "single" ], "semicolon": { "options": [ "always" ] }, "space-before-function-paren": { "options": { "anonymous": "never", "asyncArrow": "always", "constructor": "never", "method": "never", "named": "never" } }, "typedef": [ true, "call-signature" ], "typedef-whitespace": { "options": [ { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" }, { "call-signature": "onespace", "index-signature": "onespace", "parameter": "onespace", "property-declaration": "onespace", "variable-declaration": "onespace" } ] }, "variable-name": { "options": [ "ban-keywords", "check-format", "allow-pascal-case" ] }, "whitespace": { "options": [ "check-branch", "check-decl", "check-operator", "check-separator", "check-type", "check-typecast" ] }, "component-class-suffix": true, "contextual-lifecycle": true, "directive-class-suffix": true, "no-conflicting-lifecycle": true, "no-host-metadata-property": true, "no-input-rename": true, "no-inputs-metadata-property": true, "no-output-native": true, "no-output-on-prefix": true, "no-output-rename": true, "no-outputs-metadata-property": true, "template-banana-in-box": true, "template-no-negated-async": true, "use-lifecycle-interface": true, "use-pipe-transform-interface": true, "directive-selector": [ true, "attribute", "app", "camelCase" ], "component-selector": [ true, "element", "app", "kebab-case" ] } } ================================================ FILE: examples/basic-crud-application/README.md ================================================ # Basic CRUD application with Socket.IO Please read the related [guide](https://socket.io/get-started/basic-crud-application/). This repository contains several implementations of the server: | Directory | Language | Database | Cluster? | |----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| | `server/` | TypeScript | in-memory | No | | `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) | ## Running the frontend ``` cd angular-client npm install npm start ``` ### Running the server ``` cd server npm install npm start ``` ================================================ FILE: examples/basic-crud-application/angular-client/.editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: examples/basic-crud-application/angular-client/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/basic-crud-application/angular-client/README.md ================================================ # AngularClient This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.2. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/basic-crud-application/angular-client/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "angular-client": { "projectType": "application", "schematics": {}, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/angular-client", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ "zone.js" ], "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true, "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.development.ts" } ] } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "angular-client:build:production" }, "development": { "buildTarget": "angular-client:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "angular-client:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "polyfills": [ "zone.js", "zone.js/testing" ], "tsConfig": "tsconfig.spec.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] } } } } } } ================================================ FILE: examples/basic-crud-application/angular-client/package.json ================================================ { "name": "angular-client", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { "@angular/animations": "^17.0.0", "@angular/common": "^17.0.0", "@angular/compiler": "^17.0.0", "@angular/core": "^17.0.0", "@angular/forms": "^17.0.0", "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", "rxjs": "~7.8.0", "socket.io-client": "^4.7.2", "tslib": "^2.3.0", "zone.js": "~0.14.2" }, "devDependencies": { "@angular-devkit/build-angular": "^17.0.2", "@angular/cli": "^17.0.2", "@angular/compiler-cli": "^17.0.0", "@types/jasmine": "~5.1.0", "@types/node": "^20.9.2", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.2.2" } } ================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.component.css ================================================ ================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.component.html ================================================

todos

{{todoStore.getRemaining().length}} {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left
================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.component.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it(`should have the 'angular-client' title`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.title).toEqual('angular-client'); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-client'); }); }); ================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import {type Todo, TodoStore} from "./store"; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, RouterOutlet, ReactiveFormsModule], templateUrl: './app.component.html', styleUrl: './app.component.css', providers: [TodoStore] }) export class AppComponent { newTodoText = new FormControl(''); constructor(readonly todoStore: TodoStore) { } stopEditing(todo: Todo, editedTitle: string) { todo.title = editedTitle; todo.editing = false; } cancelEditingTodo(todo: Todo) { todo.editing = false; } updateEditingTodo(todo: Todo, editedTitle: string) { editedTitle = editedTitle.trim(); todo.editing = false; if (editedTitle.length === 0) { return this.todoStore.remove(todo); } todo.title = editedTitle; } editTodo(todo: Todo) { todo.editing = true; } removeCompleted() { this.todoStore.removeCompleted(); } toggleCompletion(todo: Todo) { this.todoStore.toggleCompletion(todo); } remove(todo: Todo){ this.todoStore.remove(todo); } addTodo() { if (this.newTodoText.value?.trim().length) { this.todoStore.add(this.newTodoText.value!); this.newTodoText.setValue(''); } } } ================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.config.ts ================================================ import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [provideRouter(routes)] }; ================================================ FILE: examples/basic-crud-application/angular-client/src/app/app.routes.ts ================================================ import { Routes } from '@angular/router'; export const routes: Routes = []; ================================================ FILE: examples/basic-crud-application/angular-client/src/app/store.ts ================================================ import { io, Socket } from "socket.io-client"; import { ClientEvents, ServerEvents } from "../../../common/events"; import { environment } from '../environments/environment'; import {Injectable} from "@angular/core"; export interface Todo { id: string, title: string, completed: boolean, editing: boolean, synced: boolean } const mapTodo = (todo: any) => { return { ...todo, editing: false, synced: true } } @Injectable() export class TodoStore { public todos: Array = []; private socket: Socket; constructor() { this.socket = io(environment.serverUrl); this.socket.on("connect", () => { this.socket.emit("todo:list", (res) => { if ("error" in res) { // handle the error return; } this.todos = res.data.map(mapTodo); }); }); this.socket.on("todo:created", (todo) => { this.todos.push(mapTodo(todo)); }); this.socket.on("todo:updated", (todo) => { const existingTodo = this.todos.find(t => { return t.id === todo.id }); if (existingTodo) { existingTodo.title = todo.title; existingTodo.completed = todo.completed; } }); this.socket.on("todo:deleted", (id) => { const index = this.todos.findIndex(t => { return t.id === id }); if (index !== -1) { this.todos.splice(index, 1); } }) } private getWithCompleted(completed: boolean) { return this.todos.filter((todo: Todo) => todo.completed === completed); } allCompleted() { return this.todos.length === this.getCompleted().length; } setAllTo(completed: boolean) { this.todos.forEach(todo => { todo.completed = completed; todo.synced = false; this.socket.emit("todo:update", todo, (res) => { if (res && "error" in res) { // handle the error return; } todo.synced = true; }); }); } removeCompleted() { this.getCompleted().forEach((todo) => { this.socket.emit("todo:delete", todo.id, (res) => { if (res && "error" in res) { // handle the error } }); }) this.todos = this.getRemaining(); } getRemaining() { return this.getWithCompleted(false); } getCompleted() { return this.getWithCompleted(true); } toggleCompletion(todo: Todo) { todo.completed = !todo.completed; todo.synced = false; this.socket.emit("todo:update", todo, (res) => { if (res && "error" in res) { // handle the error return; } todo.synced = true; }) } remove(todo: Todo) { this.todos.splice(this.todos.indexOf(todo), 1); this.socket.emit("todo:delete", todo.id, (res) => { if (res && "error" in res) { // handle the error } }); } add(title: string) { this.socket.emit("todo:create", { title, completed: false }, (res) => { if ("error" in res) { // handle the error return; } this.todos.push({ id: res.data, title, completed: false, editing: false, synced: true }); }); } } ================================================ FILE: examples/basic-crud-application/angular-client/src/assets/.gitkeep ================================================ ================================================ FILE: examples/basic-crud-application/angular-client/src/environments/environment.development.ts ================================================ export const environment = { serverUrl: "http://localhost:3000" }; ================================================ FILE: examples/basic-crud-application/angular-client/src/environments/environment.ts ================================================ export const environment = { serverUrl: "https://my-custom-domain.com" }; ================================================ FILE: examples/basic-crud-application/angular-client/src/index.html ================================================ AngularClient ================================================ FILE: examples/basic-crud-application/angular-client/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); ================================================ FILE: examples/basic-crud-application/angular-client/src/styles.css ================================================ /* imported from node_modules/todomvc-app-css/index.css */ html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #111111; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp h1 { position: absolute; top: -140px; width: 100%; font-size: 80px; font-weight: 200; text-align: center; color: #b83f45; -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { width: 60px; height: 34px; font-size: 0; position: absolute; top: -52px; left: -13px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all + label:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked + label:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; font-weight: 400; color: #4d4d4d; } .todo-list li.completed label { color: #cdcdcd; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { padding: 10px 15px; height: 20px; text-align: center; font-size: 15px; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #4d4d4d; font-size: 11px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ================================================ FILE: examples/basic-crud-application/angular-client/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: examples/basic-crud-application/angular-client/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": [ "ES2022", "dom" ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/basic-crud-application/angular-client/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: examples/basic-crud-application/common/events.ts ================================================ export type TodoID = string; export interface Todo { id: TodoID; completed: boolean; title: string; } interface Error { error: string; errorDetails?: { message: string; path: Array; type: string; }[]; } interface Success { data: T; } export type Response = Error | Success; export interface ServerEvents { "todo:created": (todo: Todo) => void; "todo:updated": (todo: Todo) => void; "todo:deleted": (id: TodoID) => void; } export interface ClientEvents { "todo:list": (callback: (res: Response) => void) => void; "todo:create": ( payload: Omit, callback: (res: Response) => void ) => void; "todo:read": (id: TodoID, callback: (res: Response) => void) => void; "todo:update": ( payload: Todo, callback: (res?: Response) => void ) => void; "todo:delete": (id: TodoID, callback: (res?: Response) => void) => void; } ================================================ FILE: examples/basic-crud-application/server/lib/app.ts ================================================ import { Server as HttpServer } from "http"; import { Server, ServerOptions } from "socket.io"; import { ClientEvents, ServerEvents } from "../../common/events"; import { TodoRepository } from "./todo-management/todo.repository"; import createTodoHandlers from "./todo-management/todo.handlers"; export interface Components { todoRepository: TodoRepository; } export function createApplication( httpServer: HttpServer, components: Components, serverOptions: Partial = {} ): Server { const io = new Server(httpServer, serverOptions); const { createTodo, readTodo, updateTodo, deleteTodo, listTodo, } = createTodoHandlers(components); io.on("connection", (socket) => { socket.on("todo:create", createTodo); socket.on("todo:read", readTodo); socket.on("todo:update", updateTodo); socket.on("todo:delete", deleteTodo); socket.on("todo:list", listTodo); }); return io; } ================================================ FILE: examples/basic-crud-application/server/lib/index.ts ================================================ import { createServer } from "http"; import { createApplication } from "./app"; import { InMemoryTodoRepository } from "./todo-management/todo.repository"; const httpServer = createServer(); createApplication( httpServer, { todoRepository: new InMemoryTodoRepository(), }, { cors: { origin: ["http://localhost:4200"], }, } ); httpServer.listen(3000); ================================================ FILE: examples/basic-crud-application/server/lib/todo-management/todo.handlers.ts ================================================ import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util"; import { v4 as uuid } from "uuid"; import { Components } from "../app"; import Joi = require("joi"); import { Todo, TodoID, ClientEvents, Response, ServerEvents, } from "../../../common/events"; import { Socket } from "socket.io"; const idSchema = Joi.string().guid({ version: "uuidv4", }); const todoSchema = Joi.object({ id: idSchema.alter({ create: (schema) => schema.forbidden(), update: (schema) => schema.required(), }), title: Joi.string().max(256).required(), completed: Joi.boolean().required(), }); export default function ({ todoRepository }: Components) { return { createTodo: async function ( payload: Omit, callback: (res: Response) => void ) { // @ts-ignore const socket: Socket = this; // validate the payload const { error, value } = todoSchema.tailor("create").validate(payload, { abortEarly: false, stripUnknown: true, }); if (error) { return callback({ error: Errors.INVALID_PAYLOAD, errorDetails: mapErrorDetails(error.details), }); } value.id = uuid(); // persist the entity try { await todoRepository.save(value); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } // acknowledge the creation callback({ data: value.id, }); // notify the other users socket.broadcast.emit("todo:created", value); }, readTodo: async function ( id: TodoID, callback: (res: Response) => void ) { const { error } = idSchema.validate(id); if (error) { return callback({ error: Errors.ENTITY_NOT_FOUND, }); } try { const todo = await todoRepository.findById(id); callback({ data: todo, }); } catch (e) { callback({ error: sanitizeErrorMessage(e), }); } }, updateTodo: async function ( payload: Todo, callback: (res?: Response) => void ) { // @ts-ignore const socket: Socket = this; const { error, value } = todoSchema.tailor("update").validate(payload, { abortEarly: false, stripUnknown: true, }); if (error) { return callback({ error: Errors.INVALID_PAYLOAD, errorDetails: mapErrorDetails(error.details), }); } try { await todoRepository.save(value); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } callback(); socket.broadcast.emit("todo:updated", value); }, deleteTodo: async function ( id: TodoID, callback: (res?: Response) => void ) { // @ts-ignore const socket: Socket = this; const { error } = idSchema.validate(id); if (error) { return callback({ error: Errors.ENTITY_NOT_FOUND, }); } try { await todoRepository.deleteById(id); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } callback(); socket.broadcast.emit("todo:deleted", id); }, listTodo: async function (callback: (res: Response) => void) { try { callback({ data: await todoRepository.findAll(), }); } catch (e) { callback({ error: sanitizeErrorMessage(e), }); } }, }; } ================================================ FILE: examples/basic-crud-application/server/lib/todo-management/todo.repository.ts ================================================ import { Errors } from "../util"; import { Todo, TodoID } from "../../../common/events"; abstract class CrudRepository { abstract findAll(): Promise; abstract findById(id: ID): Promise; abstract save(entity: T): Promise; abstract deleteById(id: ID): Promise; } export abstract class TodoRepository extends CrudRepository {} export class InMemoryTodoRepository extends TodoRepository { private readonly todos: Map = new Map(); findAll(): Promise { const entities = Array.from(this.todos.values()); return Promise.resolve(entities); } findById(id: TodoID): Promise { if (this.todos.has(id)) { return Promise.resolve(this.todos.get(id)!); } else { return Promise.reject(Errors.ENTITY_NOT_FOUND); } } save(entity: Todo): Promise { this.todos.set(entity.id, entity); return Promise.resolve(); } deleteById(id: TodoID): Promise { const deleted = this.todos.delete(id); if (deleted) { return Promise.resolve(); } else { return Promise.reject(Errors.ENTITY_NOT_FOUND); } } } ================================================ FILE: examples/basic-crud-application/server/lib/util.ts ================================================ import { ValidationErrorItem } from "joi"; export enum Errors { ENTITY_NOT_FOUND = "entity not found", INVALID_PAYLOAD = "invalid payload", } const errorValues: string[] = Object.values(Errors); export function sanitizeErrorMessage(message: any) { if (typeof message === "string" && errorValues.includes(message)) { return message; } else { return "an unknown error has occurred"; } } export function mapErrorDetails(details: ValidationErrorItem[]) { return details.map((item) => ({ message: item.message, path: item.path, type: item.type, })); } ================================================ FILE: examples/basic-crud-application/server/package.json ================================================ { "name": "basic-crud-server", "version": "0.0.1", "description": "Server for the Basic CRUD Socket.IO example", "main": "dist/lib/index.js", "scripts": { "start": "ts-node lib/index.ts", "build": "tsc", "test": "nyc mocha --require ts-node/register test/**/*.ts" }, "repository": { "type": "git", "url": "git+https://github.com/socketio/socket.io.git" }, "author": "Damien Arrachequesne ", "license": "MIT", "bugs": { "url": "https://github.com/socketio/socket.io/issues" }, "homepage": "https://github.com/socketio/socket.io#readme", "dependencies": { "joi": "^17.4.0", "socket.io": "^4.0.1", "uuid": "^8.3.2" }, "devDependencies": { "@types/chai": "^4.2.16", "@types/mocha": "^10.0.0", "@types/uuid": "^8.3.0", "chai": "^4.3.4", "mocha": "^10.0.0", "nyc": "^15.1.0", "socket.io-client": "^4.0.1", "ts-node": "^10.9.1", "typescript": "^4.2.4" } } ================================================ FILE: examples/basic-crud-application/server/test/todo-management/todo.tests.ts ================================================ import { createApplication } from "../../lib/app"; import { createServer, Server } from "http"; import { InMemoryTodoRepository, TodoRepository, } from "../../lib/todo-management/todo.repository"; import { AddressInfo } from "net"; import { io, Socket } from "socket.io-client"; import { ClientEvents, ServerEvents } from "../../lib/events"; import { expect } from "chai"; const createPartialDone = (count: number, done: () => void) => { let i = 0; return () => { if (++i === count) { done(); } }; }; describe("todo management", () => { let httpServer: Server, socket: Socket, otherSocket: Socket, todoRepository: TodoRepository; beforeEach((done) => { const partialDone = createPartialDone(2, done); httpServer = createServer(); todoRepository = new InMemoryTodoRepository(); createApplication(httpServer, { todoRepository, }); httpServer.listen(() => { const port = (httpServer.address() as AddressInfo).port; socket = io(`http://localhost:${port}`); socket.on("connect", partialDone); otherSocket = io(`http://localhost:${port}`); otherSocket.on("connect", partialDone); }); }); afterEach(() => { httpServer.close(); socket.disconnect(); otherSocket.disconnect(); }); describe("create todo", () => { it("should create a todo entity", (done) => { const partialDone = createPartialDone(2, done); socket.emit( "todo:create", { title: "lorem ipsum", completed: false, }, async (res) => { if ("error" in res) { return done(new Error("should not happen")); } expect(res.data).to.be.a("string"); const storedEntity = await todoRepository.findById(res.data); expect(storedEntity).to.eql({ id: res.data, title: "lorem ipsum", completed: false, }); partialDone(); } ); otherSocket.on("todo:created", (todo) => { expect(todo.id).to.be.a("string"); expect(todo.title).to.eql("lorem ipsum"); expect(todo.completed).to.eql(false); partialDone(); }); }); it("should fail with an invalid entity", (done) => { const incompleteTodo = { completed: "false", description: true, }; // @ts-ignore socket.emit("todo:create", incompleteTodo, (res) => { if (!("error" in res)) { return done(new Error("should not happen")); } expect(res.error).to.eql("invalid payload"); expect(res.errorDetails).to.eql([ { message: '"title" is required', path: ["title"], type: "any.required", }, ]); done(); }); otherSocket.on("todo:created", () => { done(new Error("should not happen")); }); }); }); describe("read todo", () => { it("should return a todo entity", (done) => { todoRepository.save({ id: "254dbf85-f5b9-4675-b913-acab5d600884", title: "lorem ipsum", completed: true, }); socket.emit( "todo:read", "254dbf85-f5b9-4675-b913-acab5d600884", (res) => { if ("error" in res) { return done(new Error("should not happen")); } expect(res.data.id).to.eql("254dbf85-f5b9-4675-b913-acab5d600884"); expect(res.data.title).to.eql("lorem ipsum"); expect(res.data.completed).to.eql(true); done(); } ); }); it("should fail with an invalid ID", (done) => { socket.emit("todo:read", "123", (res) => { if ("error" in res) { expect(res.error).to.eql("entity not found"); done(); } else { done(new Error("should not happen")); } }); }); it("should fail with an unknown entity", (done) => { socket.emit( "todo:read", "6edcf81e-7049-40e0-8497-9cdd52414f75", (res) => { if ("error" in res) { expect(res.error).to.eql("entity not found"); done(); } else { done(new Error("should not happen")); } } ); }); }); describe("update todo", () => { it("should update a todo entity", (done) => { const partialDone = createPartialDone(2, done); todoRepository.save({ id: "c7790b35-6bbb-45dd-8d67-a281ca407b43", title: "lorem ipsum", completed: true, }); socket.emit( "todo:update", { id: "c7790b35-6bbb-45dd-8d67-a281ca407b43", title: "dolor sit amet", completed: true, }, async () => { const storedEntity = await todoRepository.findById( "c7790b35-6bbb-45dd-8d67-a281ca407b43" ); expect(storedEntity).to.eql({ id: "c7790b35-6bbb-45dd-8d67-a281ca407b43", title: "dolor sit amet", completed: true, }); partialDone(); } ); otherSocket.on("todo:updated", (todo) => { expect(todo.title).to.eql("dolor sit amet"); expect(todo.completed).to.eql(true); partialDone(); }); }); it("should fail with an invalid entity", (done) => { const incompleteTodo = { id: "123", completed: "false", description: true, }; // @ts-ignore socket.emit("todo:update", incompleteTodo, (res) => { if (!(res && "error" in res)) { return done(new Error("should not happen")); } expect(res.error).to.eql("invalid payload"); expect(res.errorDetails).to.eql([ { message: '"id" must be a valid GUID', path: ["id"], type: "string.guid", }, { message: '"title" is required', path: ["title"], type: "any.required", }, ]); done(); }); otherSocket.on("todo:updated", () => { done(new Error("should not happen")); }); }); }); describe("delete todo", () => { it("should delete a todo entity", (done) => { const partialDone = createPartialDone(2, done); const id = "58960ab2-4e78-4ced-8079-134f12179d46"; todoRepository.save({ id, title: "lorem ipsum", completed: true, }); socket.emit("todo:delete", id, async () => { try { await todoRepository.findById(id); } catch (e) { partialDone(); } }); otherSocket.on("todo:deleted", (id) => { expect(id).to.eql("58960ab2-4e78-4ced-8079-134f12179d46"); partialDone(); }); }); it("should fail with an invalid ID", (done) => { socket.emit("todo:delete", "123", (res) => { if (!(res && "error" in res)) { return done(new Error("should not happen")); } expect(res.error).to.eql("entity not found"); done(); }); otherSocket.on("todo:deleted", () => { done(new Error("should not happen")); }); }); }); describe("list todo", () => { it("should return a list of entities", (done) => { todoRepository.save({ id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2", title: "lorem ipsum", completed: false, }); todoRepository.save({ id: "5f56fb59-a887-4984-93bf-eb39b4170a35", title: "dolor sit amet", completed: true, }); socket.emit("todo:list", (res) => { if ("error" in res) { return done(new Error("should not happen")); } expect(res.data).to.eql([ { id: "d445db6d-9d55-4ff2-88ae-bd1f81c299d2", title: "lorem ipsum", completed: false, }, { id: "5f56fb59-a887-4984-93bf-eb39b4170a35", title: "dolor sit amet", completed: true, }, ]); done(); }); }); }); }); ================================================ FILE: examples/basic-crud-application/server/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist", "module": "commonjs", "target": "es2017", "strict": true }, "include": [ "./lib/**/*" ] } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/README.md ================================================ A basic TODO project. | Characteristic | | |----------------|-------------------------------------------------------------------------------------------| | Language | plain JavaScript | | Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | | Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) | ## Usage ``` $ docker-compose up -d $ npm install $ npm start ``` ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/docker-compose.yml ================================================ version: "3" services: postgres: image: postgres:12 ports: - "5432:5432" environment: POSTGRES_PASSWORD: "changeit" ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/app.js ================================================ import { Server } from "socket.io"; import createTodoHandlers from "./todo-management/todo.handlers.js"; import { setupWorker } from "@socket.io/sticky"; import { createAdapter } from "@socket.io/postgres-adapter"; export function createApplication(httpServer, components, serverOptions = {}) { const io = new Server(httpServer, serverOptions); const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } = createTodoHandlers(components); io.on("connection", (socket) => { socket.on("todo:create", createTodo); socket.on("todo:read", readTodo); socket.on("todo:update", updateTodo); socket.on("todo:delete", deleteTodo); socket.on("todo:list", listTodo); }); // enable sticky session in the cluster (to remove in standalone mode) setupWorker(io); io.adapter(createAdapter(components.connectionPool)); return io; } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/cluster.js ================================================ import cluster from "cluster"; import { createServer } from "http"; import { setupMaster } from "@socket.io/sticky"; import { cpus } from "os"; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); const httpServer = createServer(); setupMaster(httpServer, { loadBalancingMethod: "least-connection", }); httpServer.listen(3000); for (let i = 0; i < cpus().length; i++) { cluster.fork(); } cluster.on("exit", (worker) => { console.log(`Worker ${worker.process.pid} died`); cluster.fork(); }); } else { console.log(`Worker ${process.pid} started`); import("./index.js"); } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/index.js ================================================ import { createServer } from "http"; import { createApplication } from "./app.js"; import { Sequelize } from "sequelize"; import pg from "pg"; import { PostgresTodoRepository } from "./todo-management/todo.repository.js"; const httpServer = createServer(); const sequelize = new Sequelize("postgres", "postgres", "changeit", { dialect: "postgres", }); const connectionPool = new pg.Pool({ user: "postgres", host: "localhost", database: "postgres", password: "changeit", port: 5432, }); createApplication( httpServer, { connectionPool, todoRepository: new PostgresTodoRepository(sequelize), }, { cors: { origin: ["http://localhost:4200"], }, } ); const main = async () => { // create the tables if they do not exist already await sequelize.sync(); // create the table needed by the postgres adapter await connectionPool.query(` CREATE TABLE IF NOT EXISTS socket_io_attachments ( id bigserial UNIQUE, created_at timestamptz DEFAULT NOW(), payload bytea ); `); // uncomment when running in standalone mode // httpServer.listen(3000); }; main(); ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/todo-management/todo.handlers.js ================================================ import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js"; import { v4 as uuid } from "uuid"; import Joi from "joi"; const idSchema = Joi.string().guid({ version: "uuidv4", }); const todoSchema = Joi.object({ id: idSchema.alter({ create: (schema) => schema.forbidden(), update: (schema) => schema.required(), }), title: Joi.string().max(256).required(), completed: Joi.boolean().required(), }); export default function (components) { const { todoRepository } = components; return { createTodo: async function (payload, callback) { const socket = this; // validate the payload const { error, value } = todoSchema.tailor("create").validate(payload, { abortEarly: false, stripUnknown: true, }); if (error) { return callback({ error: Errors.INVALID_PAYLOAD, errorDetails: mapErrorDetails(error.details), }); } value.id = uuid(); // persist the entity try { await todoRepository.save(value); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } // acknowledge the creation callback({ data: value.id, }); // notify the other users socket.broadcast.emit("todo:created", value); }, readTodo: async function (id, callback) { const { error } = idSchema.validate(id); if (error) { return callback({ error: Errors.ENTITY_NOT_FOUND, }); } try { const todo = await todoRepository.findById(id); callback({ data: todo, }); } catch (e) { callback({ error: sanitizeErrorMessage(e), }); } }, updateTodo: async function (payload, callback) { const socket = this; const { error, value } = todoSchema.tailor("update").validate(payload, { abortEarly: false, stripUnknown: true, }); if (error) { return callback({ error: Errors.INVALID_PAYLOAD, errorDetails: mapErrorDetails(error.details), }); } try { await todoRepository.save(value); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } callback(); socket.broadcast.emit("todo:updated", value); }, deleteTodo: async function (id, callback) { const socket = this; const { error } = idSchema.validate(id); if (error) { return callback({ error: Errors.ENTITY_NOT_FOUND, }); } try { await todoRepository.deleteById(id); } catch (e) { return callback({ error: sanitizeErrorMessage(e), }); } callback(); socket.broadcast.emit("todo:deleted", id); }, listTodo: async function (callback) { try { callback({ data: await todoRepository.findAll(), }); } catch (e) { callback({ error: sanitizeErrorMessage(e), }); } }, }; } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/todo-management/todo.repository.js ================================================ import { Errors } from "../util.js"; import { Model, DataTypes } from "sequelize"; class CrudRepository { findAll() {} findById(id) {} save(entity) {} deleteById(id) {} } export class TodoRepository extends CrudRepository {} class Todo extends Model {} export class PostgresTodoRepository extends TodoRepository { constructor(sequelize) { super(); this.sequelize = sequelize; Todo.init( { id: { type: DataTypes.STRING, primaryKey: true, allowNull: false, }, title: { type: DataTypes.STRING, }, completed: { type: DataTypes.BOOLEAN, }, }, { sequelize, tableName: "todos", } ); } findAll() { return this.sequelize.transaction((transaction) => { return Todo.findAll({ transaction }); }); } async findById(id) { return this.sequelize.transaction(async (transaction) => { const todo = await Todo.findByPk(id, { transaction }); if (!todo) { throw Errors.ENTITY_NOT_FOUND; } return todo; }); } save(entity) { return this.sequelize.transaction((transaction) => { return Todo.upsert(entity, { transaction }); }); } async deleteById(id) { return this.sequelize.transaction(async (transaction) => { const count = await Todo.destroy({ where: { id }, transaction }); if (count === 0) { throw Errors.ENTITY_NOT_FOUND; } }); } } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/lib/util.js ================================================ export const Errors = { ENTITY_NOT_FOUND: "entity not found", INVALID_PAYLOAD: "invalid payload", }; const errorValues = Object.values(Errors); export function sanitizeErrorMessage(message) { if (typeof message === "string" && errorValues.includes(message)) { return message; } else { return "an unknown error has occurred"; } } export function mapErrorDetails(details) { return details.map((item) => ({ message: item.message, path: item.path, type: item.type, })); } ================================================ FILE: examples/basic-crud-application/server-postgres-cluster/package.json ================================================ { "name": "basic-crud-server", "version": "0.0.1", "description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)", "main": "lib/cluster.js", "type": "module", "scripts": { "start": "node lib/cluster.js" }, "repository": { "type": "git", "url": "git+https://github.com/socketio/socket.io.git" }, "author": "Damien Arrachequesne ", "license": "MIT", "bugs": { "url": "https://github.com/socketio/socket.io/issues" }, "homepage": "https://github.com/socketio/socket.io#readme", "dependencies": { "@socket.io/postgres-adapter": "^0.2.0", "@socket.io/sticky": "^1.0.1", "joi": "^17.4.0", "pg": "^8.7.3", "pg-hstore": "^2.3.4", "sequelize": "^6.18.0", "socket.io": "^4.0.1", "uuid": "^8.3.2" } } ================================================ FILE: examples/basic-crud-application/vue-client/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/basic-crud-application/vue-client/README.md ================================================ # vue-client ## Project setup ``` yarn install ``` ### Compiles and hot-reloads for development ``` yarn serve ``` ### Compiles and minifies for production ``` yarn build ``` ### Lints and fixes files ``` yarn lint ``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). ================================================ FILE: examples/basic-crud-application/vue-client/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: examples/basic-crud-application/vue-client/jsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "esnext", "baseUrl": "./", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] } } ================================================ FILE: examples/basic-crud-application/vue-client/package.json ================================================ { "name": "vue-client", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve --port 4200", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.8.3", "pinia": "^2.1.7", "socket.io-client": "^4.7.2", "vue": "^3.2.13" }, "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-service": "~5.0.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/vue3-essential", "eslint:recommended" ], "parserOptions": { "parser": "@babel/eslint-parser" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead", "not ie 11" ] } ================================================ FILE: examples/basic-crud-application/vue-client/public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: examples/basic-crud-application/vue-client/public/styles.css ================================================ /* imported from node_modules/todomvc-app-css/index.css */ html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #111111; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: rgba(0, 0, 0, 0.4); } .todoapp h1 { position: absolute; top: -140px; width: 100%; font-size: 80px; font-weight: 200; text-align: center; color: #b83f45; -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { width: 60px; height: 34px; font-size: 0; position: absolute; top: -52px; left: -13px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all + label:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked + label:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; font-weight: 400; color: #4d4d4d; } .todo-list li.completed label { color: #cdcdcd; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { padding: 10px 15px; height: 20px; text-align: center; font-size: 15px; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #4d4d4d; font-size: 11px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } ================================================ FILE: examples/basic-crud-application/vue-client/src/App.vue ================================================ ================================================ FILE: examples/basic-crud-application/vue-client/src/main.js ================================================ import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; const pinia = createPinia(); const app = createApp(App); app.use(pinia); app.mount("#app"); ================================================ FILE: examples/basic-crud-application/vue-client/src/socket.js ================================================ import { io } from "socket.io-client"; // "undefined" means the URL will be computed from the `window.location` object const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000"; export const socket = io(URL); ================================================ FILE: examples/basic-crud-application/vue-client/src/stores/todo.js ================================================ import { defineStore } from "pinia"; import { socket } from "@/socket"; export const useTodoStore = defineStore("todo", { state: () => ({ todos: [], }), getters: { remaining(state) { let count = 0; state.todos.forEach((todo) => { if (!todo.completed) { count++; } }); return count; }, }, actions: { bindEvents() { socket.on("connect", () => { socket.emit("todo:list", (res) => { this.todos = res.data; }); }); socket.on("todo:created", (todo) => { this.todos.push(todo); }); socket.on("todo:updated", (todo) => { const existingTodo = this.todos.find((t) => { return t.id === todo.id; }); if (existingTodo) { existingTodo.title = todo.title; existingTodo.completed = todo.completed; } }); socket.on("todo:deleted", (id) => { const i = this.todos.findIndex((t) => { return t.id === id; }); if (i !== -1) { this.todos.splice(i, 1); } }); }, add(title) { const todo = { id: Date.now(), title, completed: false, }; this.todos.push(todo); socket.emit("todo:create", { title, completed: false }, (res) => { todo.id = res.data; }); }, setTitle(todo, title) { todo.title = title; socket.emit("todo:update", todo, () => {}); }, delete(todo) { const i = this.todos.findIndex((t) => { return t.id === todo.id; }); if (i !== -1) { this.todos.splice(i, 1); socket.emit("todo:delete", todo.id, () => {}); } }, deleteCompleted() { this.todos.forEach((todo) => { if (todo.completed) { socket.emit("todo:delete", todo.id, () => {}); } }); this.todos = this.todos.filter((t) => { return !t.completed; }); }, toggleOne(todo) { todo.completed = !todo.completed; socket.emit("todo:update", todo, () => {}); }, toggleAll(onlyActive) { this.todos.forEach((todo) => { if (!onlyActive || !todo.completed) { this.toggleOne(todo); } }); }, }, }); ================================================ FILE: examples/basic-crud-application/vue-client/vue.config.js ================================================ const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true }) ================================================ FILE: examples/basic-websocket-client/README.md ================================================ # Basic Socket.IO client Please check the associated guide: https://socket.io/how-to/build-a-basic-client Content: ``` ├── bundle │ └── socket.io.min.js ├── src │ └── index.js ├── test │ └── index.js ├── check-bundle-size.js ├── package.json ├── README.md └── rollup.config.js ``` ================================================ FILE: examples/basic-websocket-client/check-bundle-size.js ================================================ import { rollup } from "rollup"; import terser from "@rollup/plugin-terser"; import { brotliCompressSync } from "node:zlib"; const rollupBuild = await rollup({ input: "./src/index.js" }); const rollupOutput = await rollupBuild.generate({ format: "esm", plugins: [terser()], }); const bundleAsString = rollupOutput.output[0].code; const brotliedBundle = brotliCompressSync(Buffer.from(bundleAsString)); console.log(`Bundle size: ${brotliedBundle.length} B`); ================================================ FILE: examples/basic-websocket-client/package.json ================================================ { "type": "module", "devDependencies": { "@rollup/plugin-terser": "^0.4.0", "chai": "^4.3.7", "mocha": "^10.2.0", "prettier": "^2.8.4", "rollup": "^3.20.2", "socket.io": "^4.6.1", "ws": "^8.18.3" }, "scripts": { "bundle": "rollup -c", "check-bundle-size": "node check-bundle-size.js", "format": "prettier -w src/ test/", "test": "mocha" } } ================================================ FILE: examples/basic-websocket-client/rollup.config.js ================================================ import terser from "@rollup/plugin-terser"; export default { input: "./src/index.js", output: { file: "./bundle/socket.io.min.js", format: "esm", plugins: [terser()], } }; ================================================ FILE: examples/basic-websocket-client/src/index.js ================================================ class EventEmitter { #listeners = new Map(); on(event, listener) { let listeners = this.#listeners.get(event); if (!listeners) { this.#listeners.set(event, (listeners = [])); } listeners.push(listener); } emit(event, ...args) { const listeners = this.#listeners.get(event); if (listeners) { for (const listener of listeners) { listener.apply(null, args); } } } } const EIOPacketType = { OPEN: "0", CLOSE: "1", PING: "2", PONG: "3", MESSAGE: "4", }; const SIOPacketType = { CONNECT: 0, DISCONNECT: 1, EVENT: 2, }; function noop() {} class Socket extends EventEmitter { id; connected = false; #uri; #opts; #ws; #pingTimeoutTimer; #pingTimeoutDelay; #sendBuffer = []; #reconnectTimer; #shouldReconnect = true; constructor(uri, opts) { super(); this.#uri = uri; this.#opts = Object.assign( { path: "/socket.io/", reconnectionDelay: 2000, }, opts ); this.#open(); } #open() { this.#ws = new WebSocket(this.#createUrl()); this.#ws.onmessage = ({ data }) => this.#onMessage(data); // dummy handler for Node.js this.#ws.onerror = noop; this.#ws.onclose = () => this.#onClose("transport close"); } #createUrl() { const uri = this.#uri.replace(/^http/, "ws"); const queryParams = "?EIO=4&transport=websocket"; return `${uri}${this.#opts.path}${queryParams}`; } #onMessage(data) { if (typeof data !== "string") { // TODO handle binary payloads return; } switch (data[0]) { case EIOPacketType.OPEN: this.#onOpen(data); break; case EIOPacketType.CLOSE: this.#onClose("transport close"); break; case EIOPacketType.PING: this.#resetPingTimeout(); this.#send(EIOPacketType.PONG); break; case EIOPacketType.MESSAGE: let packet; try { packet = decode(data); } catch (e) { return this.#onClose("parse error"); } this.#onPacket(packet); break; default: this.#onClose("parse error"); break; } } #onOpen(data) { let handshake; try { handshake = JSON.parse(data.substring(1)); } catch (e) { return this.#onClose("parse error"); } this.#pingTimeoutDelay = handshake.pingInterval + handshake.pingTimeout; this.#resetPingTimeout(); this.#doConnect(); } #onPacket(packet) { switch (packet.type) { case SIOPacketType.CONNECT: this.#onConnect(packet); break; case SIOPacketType.DISCONNECT: this.#shouldReconnect = false; this.#onClose("io server disconnect"); break; case SIOPacketType.EVENT: super.emit.apply(this, packet.data); break; default: this.#onClose("parse error"); break; } } #onConnect(packet) { this.id = packet.data.sid; this.connected = true; this.#sendBuffer.forEach((packet) => this.#sendPacket(packet)); this.#sendBuffer.slice(0); super.emit("connect"); } #onClose(reason) { if (this.#ws) { this.#ws.onclose = noop; this.#ws.close(); } clearTimeout(this.#pingTimeoutTimer); clearTimeout(this.#reconnectTimer); if (this.connected) { this.connected = false; this.id = undefined; super.emit("disconnect", reason); } else { super.emit("connect_error", reason); } if (this.#shouldReconnect) { this.#reconnectTimer = setTimeout( () => this.#open(), this.#opts.reconnectionDelay ); } } #resetPingTimeout() { clearTimeout(this.#pingTimeoutTimer); this.#pingTimeoutTimer = setTimeout(() => { this.#onClose("ping timeout"); }, this.#pingTimeoutDelay); } #send(data) { if (this.#ws.readyState === WebSocket.OPEN) { this.#ws.send(data); } } #sendPacket(packet) { this.#send(EIOPacketType.MESSAGE + encode(packet)); } #doConnect() { this.#sendPacket({ type: SIOPacketType.CONNECT }); } emit(...args) { const packet = { type: SIOPacketType.EVENT, data: args, }; if (this.connected) { this.#sendPacket(packet); } else { this.#sendBuffer.push(packet); } } disconnect() { this.#shouldReconnect = false; this.#onClose("io client disconnect"); } } function encode(packet) { let output = "" + packet.type; if (packet.data) { output += JSON.stringify(packet.data); } return output; } function decode(data) { let i = 1; // skip "4" prefix const packet = { type: parseInt(data.charAt(i++), 10), }; if (data.charAt(i)) { packet.data = JSON.parse(data.substring(i)); } if (!isPacketValid(packet)) { throw new Error("invalid format"); } return packet; } function isPacketValid(packet) { switch (packet.type) { case SIOPacketType.CONNECT: return typeof packet.data === "object"; case SIOPacketType.DISCONNECT: return packet.data === undefined; case SIOPacketType.EVENT: { const args = packet.data; return ( Array.isArray(args) && args.length > 0 && typeof args[0] === "string" ); } default: return false; } } export function io(uri, opts) { if (typeof uri !== "string") { opts = uri; uri = location.origin; } return new Socket(uri, opts); } ================================================ FILE: examples/basic-websocket-client/test/index.js ================================================ import { createServer } from "node:http"; import { io as ioc } from "../src/index.js"; import { WebSocket } from "ws"; import { Server } from "socket.io"; import { expect } from "chai"; // @ts-ignore for Node.js globalThis.WebSocket = WebSocket; function waitFor(emitter, eventName) { return new Promise((resolve) => { emitter.on(eventName, resolve); }); } function sleep(delay) { return new Promise((resolve) => { setTimeout(resolve, delay); }); } describe("basic client", () => { let io, port, socket; beforeEach(() => { const httpServer = createServer(); io = new Server(httpServer); httpServer.listen(0); port = httpServer.address().port; }); afterEach(() => { io.close(); socket.disconnect(); }); it("should connect", async () => { socket = ioc(`ws://localhost:${port}`); await waitFor(socket, "connect"); expect(socket.connected).to.eql(true); expect(socket.id).to.be.a("string"); }); it("should connect with 'http://' scheme", async () => { socket = ioc(`http://localhost:${port}`); await waitFor(socket, "connect"); }); it("should connect with URL inferred from 'window.location'", async () => { globalThis.location = { origin: `http://localhost:${port}`, }; socket = ioc(); await waitFor(socket, "connect"); }); it("should fail to connect to an invalid URL", async () => { socket = ioc(`http://localhost:4321`); await waitFor(socket, "connect_error"); }); it("should receive an event", async () => { io.on("connection", (socket) => { socket.emit("foo", 123); }); socket = ioc(`ws://localhost:${port}`); const value = await waitFor(socket, "foo"); expect(value).to.eql(123); }); it("should send an event (not buffered)", async () => { socket = ioc(`ws://localhost:${port}`); const [serverSocket] = await Promise.all([ waitFor(io, "connection"), waitFor(socket, "connect"), ]); socket.emit("foo", 456); const value = await waitFor(serverSocket, "foo"); expect(value).to.eql(456); }); it("should send an event (buffered)", async () => { socket = ioc(`ws://localhost:${port}`); socket.emit("foo", 789); const [serverSocket] = await Promise.all([ waitFor(io, "connection"), waitFor(socket, "connect"), ]); const value = await waitFor(serverSocket, "foo"); expect(value).to.eql(789); }); it("should reconnect", async () => { socket = ioc(`ws://localhost:${port}`, { reconnectionDelay: 50, }); await waitFor(socket, "connect"); io.close(); await waitFor(socket, "disconnect"); io.listen(port); await waitFor(socket, "connect"); }); it("should respond to PING packets", async () => { io.engine.opts.pingInterval = 50; io.engine.opts.pingTimeout = 20; socket = ioc(`ws://localhost:${port}`); await waitFor(socket, "connect"); await sleep(500); expect(socket.connected).to.eql(true); }); it("should disconnect (client side)", async () => { socket = ioc(`ws://localhost:${port}`); await waitFor(socket, "connect"); socket.disconnect(); expect(socket.connected).to.eql(false); expect(socket.id).to.eql(undefined); }); it("should disconnect (server side)", async () => { socket = ioc(`ws://localhost:${port}`); const [serverSocket] = await Promise.all([ waitFor(io, "connection"), waitFor(socket, "connect"), ]); serverSocket.disconnect(); await waitFor(socket, "disconnect"); }); }); ================================================ FILE: examples/chat/README.md ================================================ # Socket.IO Chat A simple chat demo for Socket.IO ## How to use ``` $ npm i $ npm start ``` And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable. ## Features - Multiple users can join a chat room by each entering a unique username on website load. - Users can type chat messages to the chat room. - A notification is sent to all users when a user joins or leaves the chatroom. ================================================ FILE: examples/chat/index.js ================================================ // Setup basic express server const express = require('express'); const app = express(); const path = require('path'); const server = require('http').createServer(app); const io = require('socket.io')(server); const port = process.env.PORT || 3000; server.listen(port, () => { console.log('Server listening at port %d', port); }); // Routing app.use(express.static(path.join(__dirname, 'public'))); // Chatroom let numUsers = 0; io.on('connection', (socket) => { let addedUser = false; // when the client emits 'new message', this listens and executes socket.on('new message', (data) => { // we tell the client to execute 'new message' socket.broadcast.emit('new message', { username: socket.username, message: data }); }); // when the client emits 'add user', this listens and executes socket.on('add user', (username) => { if (addedUser) return; // we store the username in the socket session for this client socket.username = username; ++numUsers; addedUser = true; socket.emit('login', { numUsers: numUsers }); // echo globally (all clients) that a person has connected socket.broadcast.emit('user joined', { username: socket.username, numUsers: numUsers }); }); // when the client emits 'typing', we broadcast it to others socket.on('typing', () => { socket.broadcast.emit('typing', { username: socket.username }); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', () => { socket.broadcast.emit('stop typing', { username: socket.username }); }); // when the user disconnects.. perform this socket.on('disconnect', () => { if (addedUser) { --numUsers; // echo globally that this client has left socket.broadcast.emit('user left', { username: socket.username, numUsers: numUsers }); } }); }); ================================================ FILE: examples/chat/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "BSD", "dependencies": { "express": "~4.17.1", "socket.io": "^4.0.0" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/chat/public/index.html ================================================ Socket.IO Chat Example
    ================================================ FILE: examples/chat/public/main.js ================================================ $(function() { const FADE_TIME = 150; // ms const TYPING_TIMER_LENGTH = 400; // ms const COLORS = [ '#e21400', '#91580f', '#f8a700', '#f78b00', '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', '#3b88eb', '#3824aa', '#a700ff', '#d300e7' ]; // Initialize variables const $window = $(window); const $usernameInput = $('.usernameInput'); // Input for username const $messages = $('.messages'); // Messages area const $inputMessage = $('.inputMessage'); // Input message input box const $loginPage = $('.login.page'); // The login page const $chatPage = $('.chat.page'); // The chatroom page const socket = io(); // Prompt for setting a username let username; let connected = false; let typing = false; let lastTypingTime; let $currentInput = $usernameInput.focus(); const addParticipantsMessage = (data) => { let message = ''; if (data.numUsers === 1) { message += `there's 1 participant`; } else { message += `there are ${data.numUsers} participants`; } log(message); } // Sets the client's username const setUsername = () => { username = cleanInput($usernameInput.val().trim()); // If the username is valid if (username) { $loginPage.fadeOut(); $chatPage.show(); $loginPage.off('click'); $currentInput = $inputMessage.focus(); // Tell the server your username socket.emit('add user', username); } } // Sends a chat message const sendMessage = () => { let message = $inputMessage.val(); // Prevent markup from being injected into the message message = cleanInput(message); // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); addChatMessage({ username, message }); // tell server to execute 'new message' and send along one parameter socket.emit('new message', message); } } // Log a message const log = (message, options) => { const $el = $('
  • ').addClass('log').text(message); addMessageElement($el, options); } // Adds the visual chat message to the message list const addChatMessage = (data, options = {}) => { // Don't fade the message in if there is an 'X was typing' const $typingMessages = getTypingMessages(data); if ($typingMessages.length !== 0) { options.fade = false; $typingMessages.remove(); } const $usernameDiv = $('') .text(data.username) .css('color', getUsernameColor(data.username)); const $messageBodyDiv = $('') .text(data.message); const typingClass = data.typing ? 'typing' : ''; const $messageDiv = $('
  • ') .data('username', data.username) .addClass(typingClass) .append($usernameDiv, $messageBodyDiv); addMessageElement($messageDiv, options); } // Adds the visual chat typing message const addChatTyping = (data) => { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message const removeChatTyping = (data) => { getTypingMessages(data).fadeOut(function () { $(this).remove(); }); } // Adds a message element to the messages and scrolls to the bottom // el - The element to add as a message // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) const addMessageElement = (el, options) => { const $el = $(el); // Setup default options if (!options) { options = {}; } if (typeof options.fade === 'undefined') { options.fade = true; } if (typeof options.prepend === 'undefined') { options.prepend = false; } // Apply options if (options.fade) { $el.hide().fadeIn(FADE_TIME); } if (options.prepend) { $messages.prepend($el); } else { $messages.append($el); } $messages[0].scrollTop = $messages[0].scrollHeight; } // Prevents input from having injected markup const cleanInput = (input) => { return $('
    ').text(input).html(); } // Updates the typing event const updateTyping = () => { if (connected) { if (!typing) { typing = true; socket.emit('typing'); } lastTypingTime = (new Date()).getTime(); setTimeout(() => { const typingTimer = (new Date()).getTime(); const timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { socket.emit('stop typing'); typing = false; } }, TYPING_TIMER_LENGTH); } } // Gets the 'X is typing' messages of a user const getTypingMessages = (data) => { return $('.typing.message').filter(function (i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function const getUsernameColor = (username) => { // Compute hash code let hash = 7; for (let i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color const index = Math.abs(hash % COLORS.length); return COLORS[index]; } // Keyboard events $window.keydown(event => { // Auto-focus the current input when a key is typed if (!(event.ctrlKey || event.metaKey || event.altKey)) { $currentInput.focus(); } // When the client hits ENTER on their keyboard if (event.which === 13) { if (username) { sendMessage(); socket.emit('stop typing'); typing = false; } else { setUsername(); } } }); $inputMessage.on('input', () => { updateTyping(); }); // Click events // Focus input when clicking anywhere on login page $loginPage.click(() => { $currentInput.focus(); }); // Focus input when clicking on the message input's border $inputMessage.click(() => { $inputMessage.focus(); }); // Socket events // Whenever the server emits 'login', log the login message socket.on('login', (data) => { connected = true; // Display the welcome message const message = 'Welcome to Socket.IO Chat – '; log(message, { prepend: true }); addParticipantsMessage(data); }); // Whenever the server emits 'new message', update the chat body socket.on('new message', (data) => { addChatMessage(data); }); // Whenever the server emits 'user joined', log it in the chat body socket.on('user joined', (data) => { log(`${data.username} joined`); addParticipantsMessage(data); }); // Whenever the server emits 'user left', log it in the chat body socket.on('user left', (data) => { log(`${data.username} left`); addParticipantsMessage(data); removeChatTyping(data); }); // Whenever the server emits 'typing', show the typing message socket.on('typing', (data) => { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message socket.on('stop typing', (data) => { removeChatTyping(data); }); socket.on('disconnect', () => { log('you have been disconnected'); }); socket.io.on('reconnect', () => { log('you have been reconnected'); if (username) { socket.emit('add user', username); } }); socket.io.on('reconnect_error', () => { log('attempt to reconnect has failed'); }); }); ================================================ FILE: examples/chat/public/style.css ================================================ /* Fix user-agent */ * { box-sizing: border-box; } html { font-weight: 300; -webkit-font-smoothing: antialiased; } html, input { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } html, body { height: 100%; margin: 0; padding: 0; } ul { list-style: none; word-wrap: break-word; } /* Pages */ .pages { height: 100%; margin: 0; padding: 0; width: 100%; } .page { height: 100%; position: absolute; width: 100%; } /* Login Page */ .login.page { background-color: #000; } .login.page .form { height: 100px; margin-top: -100px; position: absolute; text-align: center; top: 50%; width: 100%; } .login.page .form .usernameInput { background-color: transparent; border: none; border-bottom: 2px solid #fff; outline: none; padding-bottom: 15px; text-align: center; width: 400px; } .login.page .title { font-size: 200%; } .login.page .usernameInput { font-size: 200%; letter-spacing: 3px; } .login.page .title, .login.page .usernameInput { color: #fff; font-weight: 100; } /* Chat page */ .chat.page { display: none; } /* Font */ .messages { font-size: 150%; } .inputMessage { font-size: 100%; } .log { color: gray; font-size: 70%; margin: 5px; text-align: center; } /* Messages */ .chatArea { height: 100%; padding-bottom: 60px; } .messages { height: 100%; margin: 0; overflow-y: scroll; padding: 10px 20px 10px 20px; } .message.typing .messageBody { color: gray; } .username { font-weight: 700; overflow: hidden; padding-right: 15px; text-align: right; } /* Input */ .inputMessage { border: 10px solid #000; bottom: 0; height: 60px; left: 0; outline: none; padding-left: 10px; position: absolute; right: 0; width: 100%; } ================================================ FILE: examples/cluster-engine-node-cluster/README.md ================================================ # Example with `@socket.io/cluster-engine` and Node.js cluster ## How to use ```bash # run the server $ node server.js # run the client $ node client.js ``` ## Explanation The `server.js` script will create one Socket.IO server per core, each listening on the same port (`3000`). With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same worker. The `NodeClusterEngine` is a custom engine which takes care of the synchronization between the servers by using [the IPC channel](https://nodejs.org/api/cluster.html#workersendmessage-sendhandle-options-callback) and removes the need for sticky sessions when scaling horizontally. ================================================ FILE: examples/cluster-engine-node-cluster/client.js ================================================ import { io } from "socket.io-client"; const CLIENTS_COUNT = 3; for (let i = 0; i < CLIENTS_COUNT; i++) { const socket = io("ws://localhost:3000/", { // transports: ["polling"], // transports: ["websocket"], }); socket.on("connect", () => { console.log(`connected as ${socket.id}`); }); socket.on("disconnect", (reason) => { console.log(`disconnected due to ${reason}`); }); socket.on("hello", (socketId, workerId) => { console.log(`received "hello" from ${socketId} (worker: ${workerId})`); }); setInterval(() => { socket.emit("hello"); }, 2000); } ================================================ FILE: examples/cluster-engine-node-cluster/package.json ================================================ { "private": true, "name": "cluster-engine-node-cluster", "version": "0.0.1", "type": "module", "dependencies": { "@socket.io/cluster-adapter": "^0.2.2", "@socket.io/cluster-engine": "^0.1.0", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/cluster-engine-node-cluster/server.js ================================================ import cluster from "node:cluster"; import process from "node:process"; import { availableParallelism } from "node:os"; import { setupPrimary as setupPrimaryEngine, NodeClusterEngine, } from "@socket.io/cluster-engine"; import { setupPrimary as setupPrimaryAdapter, createAdapter, } from "@socket.io/cluster-adapter"; import { createServer } from "node:http"; import { Server } from "socket.io"; if (cluster.isPrimary) { console.log(`Primary ${process.pid} is running`); const numCPUs = availableParallelism(); // fork workers for (let i = 0; i < numCPUs; i++) { cluster.fork(); } setupPrimaryEngine(); setupPrimaryAdapter(); // needed for packets containing Buffer objects (you can ignore it if you only send plaintext objects) cluster.setupPrimary({ serialization: "advanced", }); cluster.on("exit", (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { const httpServer = createServer((req, res) => { res.writeHead(404).end(); }); const engine = new NodeClusterEngine(); engine.attach(httpServer, { path: "/socket.io/", }); const io = new Server({ adapter: createAdapter(), }); io.bind(engine); io.on("connection", (socket) => { socket.on("hello", () => { socket.broadcast.emit("hello", socket.id, process.pid); }); }); // workers will share the same port httpServer.listen(3000); console.log(`Worker ${process.pid} started`); } ================================================ FILE: examples/cluster-engine-redis/README.md ================================================ # Example with `@socket.io/cluster-engine` and Redis ## How to use ```bash # start the redis server $ docker compose up -d # run the server $ node server.js # run the client $ node client.js ``` ## Explanation The `server.js` script will create 3 Socket.IO servers, each listening on a distinct port (`3001`, `3002` and `3003`), and a proxy server listening on port `3000` which randomly redirects to one of those servers. With the default engine (provided by the `engine.io` package), sticky sessions would be required, so that each HTTP request of the same Engine.IO session reaches the same server. The `RedisEngine` is a custom engine which takes care of the synchronization between the servers by using [Redis pub/sub](https://redis.io/docs/latest/develop/interact/pubsub/) and removes the need for sticky sessions when scaling horizontally. ================================================ FILE: examples/cluster-engine-redis/client.js ================================================ import { io } from "socket.io-client"; const CLIENTS_COUNT = 3; for (let i = 0; i < CLIENTS_COUNT; i++) { const socket = io("ws://localhost:3000/", { // transports: ["polling"], // transports: ["websocket"], }); socket.on("connect", () => { console.log(`connected as ${socket.id}`); }); socket.on("disconnect", (reason) => { console.log(`disconnected due to ${reason}`); }); socket.on("hello", (socketId, workerId) => { console.log(`received "hello" from ${socketId} (worker: ${workerId})`); }); setInterval(() => { socket.emit("hello"); }, 2000); } ================================================ FILE: examples/cluster-engine-redis/compose.yaml ================================================ services: redis: image: redis:7 ports: - "6379:6379" ================================================ FILE: examples/cluster-engine-redis/package.json ================================================ { "private": true, "name": "cluster-engine-redis", "version": "0.0.1", "type": "module", "dependencies": { "@socket.io/cluster-engine": "^0.1.0", "@socket.io/redis-adapter": "^8.3.0", "http-proxy": "^1.18.1", "redis": "^4.6.15", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/cluster-engine-redis/server.js ================================================ import { RedisEngine } from "@socket.io/cluster-engine"; import { createServer } from "node:http"; import { createClient } from "redis"; import { Server } from "socket.io"; import { createAdapter } from "@socket.io/redis-adapter"; import proxyModule from "http-proxy"; const { createProxyServer } = proxyModule; async function initServer(port) { const httpServer = createServer((req, res) => { res.writeHead(404).end(); }); const pubClient = createClient(); const subClient = pubClient.duplicate(); await Promise.all([pubClient.connect(), subClient.connect()]); const engine = new RedisEngine(pubClient, subClient); engine.attach(httpServer, { path: "/socket.io/", }); const io = new Server({ adapter: createAdapter(pubClient, subClient), }); io.bind(engine); io.on("connection", (socket) => { socket.on("hello", () => { socket.broadcast.emit("hello", socket.id, port); }); }); httpServer.listen(port); } function initProxy() { const proxy = createProxyServer(); function randomTarget() { return [ "http://localhost:3001", "http://localhost:3002", "http://localhost:3003", ][Math.floor(Math.random() * 3)]; } const httpServer = createServer((req, res) => { proxy.web(req, res, { target: randomTarget() }); }); httpServer.on("upgrade", function (req, socket, head) { proxy.ws(req, socket, head, { target: randomTarget() }); }); httpServer.listen(3000); } await Promise.all([initServer(3001), initServer(3002), initServer(3003)]); initProxy(); ================================================ FILE: examples/cluster-haproxy/README.md ================================================ # Socket.IO Chat with haproxy & redis A simple chat demo for socket.io ## How to use Install [Docker Compose](https://docs.docker.com/compose/install/), then: ``` $ docker-compose up -d ``` And then point your browser to `http://localhost:3000`. This will start four Socket.IO nodes, behind a haproxy instance which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4.2-cookie)). Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to. ``` # you can kill a given node, the client should reconnect to another node $ docker-compose stop server-george ``` ## Features - Multiple users can join a chat room by each entering a unique username on website load. - Users can type chat messages to the chat room. - A notification is sent to all users when a user joins or leaves the chatroom. ================================================ FILE: examples/cluster-haproxy/docker-compose.yml ================================================ services: haproxy: image: haproxy:1.7-alpine volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro links: - server-john - server-paul - server-george - server-ringo ports: - "3000:80" server-john: build: ./server links: - redis expose: - "3000" environment: - NAME=John server-paul: build: ./server links: - redis expose: - "3000" environment: - NAME=Paul server-george: build: ./server links: - redis expose: - "3000" environment: - NAME=George server-ringo: build: ./server links: - redis expose: - "3000" environment: - NAME=Ringo redis: image: redis:alpine expose: - "6379" ================================================ FILE: examples/cluster-haproxy/haproxy.cfg ================================================ # Reference: http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/ global daemon maxconn 4096 nbproc 2 defaults mode http balance roundrobin option http-server-close timeout connect 5s timeout client 30s timeout client-fin 30s timeout server 30s timeout tunnel 1h default-server inter 1s rise 2 fall 1 on-marked-down shutdown-sessions option forwardfor listen chat bind *:80 default_backend nodes backend nodes option httpchk HEAD /health http-check expect status 200 cookie serverid insert server john server-john:3000 cookie john check server paul server-paul:3000 cookie paul check server george server-george:3000 cookie george check server ringo server-ringo:3000 cookie ringo check ================================================ FILE: examples/cluster-haproxy/server/Dockerfile ================================================ FROM node:14-alpine # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN npm install --prod # Bundle app source COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ] ================================================ FILE: examples/cluster-haproxy/server/index.js ================================================ // Setup basic express server var express = require('express'); var app = express(); var server = require('http').createServer(app); var io = require('socket.io')(server); var redis = require('socket.io-redis'); var port = process.env.PORT || 3000; var serverName = process.env.NAME || 'Unknown'; io.adapter(redis({ host: 'redis', port: 6379 })); server.listen(port, function () { console.log('Server listening at port %d', port); console.log('Hello, I\'m %s, how can I help?', serverName); }); // Routing app.use(express.static(__dirname + '/public')); // Health check app.head('/health', function (req, res) { res.sendStatus(200); }); // Chatroom var numUsers = 0; io.on('connection', function (socket) { socket.emit('my-name-is', serverName); var addedUser = false; // when the client emits 'new message', this listens and executes socket.on('new message', function (data) { // we tell the client to execute 'new message' socket.broadcast.emit('new message', { username: socket.username, message: data }); }); // when the client emits 'add user', this listens and executes socket.on('add user', function (username) { if (addedUser) return; // we store the username in the socket session for this client socket.username = username; ++numUsers; addedUser = true; socket.emit('login', { numUsers: numUsers }); // echo globally (all clients) that a person has connected socket.broadcast.emit('user joined', { username: socket.username, numUsers: numUsers }); }); // when the client emits 'typing', we broadcast it to others socket.on('typing', function () { socket.broadcast.emit('typing', { username: socket.username }); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', function () { socket.broadcast.emit('stop typing', { username: socket.username }); }); // when the user disconnects.. perform this socket.on('disconnect', function () { if (addedUser) { --numUsers; // echo globally that this client has left socket.broadcast.emit('user left', { username: socket.username, numUsers: numUsers }); } }); }); ================================================ FILE: examples/cluster-haproxy/server/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "BSD", "dependencies": { "express": "4.13.4", "socket.io": "^4.0.0", "socket.io-redis": "^6.0.1" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/cluster-haproxy/server/public/index.html ================================================ Socket.IO Chat Example
      ================================================ FILE: examples/cluster-haproxy/server/public/main.js ================================================ $(function() { var FADE_TIME = 150; // ms var TYPING_TIMER_LENGTH = 400; // ms var COLORS = [ '#e21400', '#91580f', '#f8a700', '#f78b00', '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', '#3b88eb', '#3824aa', '#a700ff', '#d300e7' ]; // Initialize variables var $window = $(window); var $usernameInput = $('.usernameInput'); // Input for username var $messages = $('.messages'); // Messages area var $inputMessage = $('.inputMessage'); // Input message input box var $loginPage = $('.login.page'); // The login page var $chatPage = $('.chat.page'); // The chatroom page // Prompt for setting a username var username; var connected = false; var typing = false; var lastTypingTime; var $currentInput = $usernameInput.focus(); var socket = io(); function addParticipantsMessage (data) { var message = ''; if (data.numUsers === 1) { message += "there's 1 participant"; } else { message += "there are " + data.numUsers + " participants"; } log(message); } // Sets the client's username function setUsername () { username = cleanInput($usernameInput.val().trim()); // If the username is valid if (username) { $loginPage.fadeOut(); $chatPage.show(); $loginPage.off('click'); $currentInput = $inputMessage.focus(); // Tell the server your username socket.emit('add user', username); } } // Sends a chat message function sendMessage () { var message = $inputMessage.val(); // Prevent markup from being injected into the message message = cleanInput(message); // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); addChatMessage({ username: username, message: message }); // tell server to execute 'new message' and send along one parameter socket.emit('new message', message); } } // Log a message function log (message, options) { var $el = $('
    • ').addClass('log').text(message); addMessageElement($el, options); } // Adds the visual chat message to the message list function addChatMessage (data, options) { // Don't fade the message in if there is an 'X was typing' var $typingMessages = getTypingMessages(data); options = options || {}; if ($typingMessages.length !== 0) { options.fade = false; $typingMessages.remove(); } var $usernameDiv = $('') .text(data.username) .css('color', getUsernameColor(data.username)); var $messageBodyDiv = $('') .text(data.message); var typingClass = data.typing ? 'typing' : ''; var $messageDiv = $('
    • ') .data('username', data.username) .addClass(typingClass) .append($usernameDiv, $messageBodyDiv); addMessageElement($messageDiv, options); } // Adds the visual chat typing message function addChatTyping (data) { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message function removeChatTyping (data) { getTypingMessages(data).fadeOut(function () { $(this).remove(); }); } // Adds a message element to the messages and scrolls to the bottom // el - The element to add as a message // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) function addMessageElement (el, options) { var $el = $(el); // Setup default options if (!options) { options = {}; } if (typeof options.fade === 'undefined') { options.fade = true; } if (typeof options.prepend === 'undefined') { options.prepend = false; } // Apply options if (options.fade) { $el.hide().fadeIn(FADE_TIME); } if (options.prepend) { $messages.prepend($el); } else { $messages.append($el); } $messages[0].scrollTop = $messages[0].scrollHeight; } // Prevents input from having injected markup function cleanInput (input) { return $('
      ').text(input).text(); } // Updates the typing event function updateTyping () { if (connected) { if (!typing) { typing = true; socket.emit('typing'); } lastTypingTime = (new Date()).getTime(); setTimeout(function () { var typingTimer = (new Date()).getTime(); var timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { socket.emit('stop typing'); typing = false; } }, TYPING_TIMER_LENGTH); } } // Gets the 'X is typing' messages of a user function getTypingMessages (data) { return $('.typing.message').filter(function (i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function function getUsernameColor (username) { // Compute hash code var hash = 7; for (var i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color var index = Math.abs(hash % COLORS.length); return COLORS[index]; } // Keyboard events $window.keydown(function (event) { // Auto-focus the current input when a key is typed if (!(event.ctrlKey || event.metaKey || event.altKey)) { $currentInput.focus(); } // When the client hits ENTER on their keyboard if (event.which === 13) { if (username) { sendMessage(); socket.emit('stop typing'); typing = false; } else { setUsername(); } } }); $inputMessage.on('input', function() { updateTyping(); }); // Click events // Focus input when clicking anywhere on login page $loginPage.click(function () { $currentInput.focus(); }); // Focus input when clicking on the message input's border $inputMessage.click(function () { $inputMessage.focus(); }); // Socket events // Whenever the server emits 'login', log the login message socket.on('login', function (data) { connected = true; // Display the welcome message var message = "Welcome to Socket.IO Chat – "; log(message, { prepend: true }); addParticipantsMessage(data); }); // Whenever the server emits 'new message', update the chat body socket.on('new message', function (data) { addChatMessage(data); }); // Whenever the server emits 'user joined', log it in the chat body socket.on('user joined', function (data) { log(data.username + ' joined'); addParticipantsMessage(data); }); // Whenever the server emits 'user left', log it in the chat body socket.on('user left', function (data) { log(data.username + ' left'); addParticipantsMessage(data); removeChatTyping(data); }); // Whenever the server emits 'typing', show the typing message socket.on('typing', function (data) { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message socket.on('stop typing', function (data) { removeChatTyping(data); }); socket.on('disconnect', function () { log('you have been disconnected'); }); socket.on('reconnect', function () { log('you have been reconnected'); if (username) { socket.emit('add user', username); } }); socket.on('reconnect_error', function () { log('attempt to reconnect has failed'); }); socket.on('my-name-is', function (serverName) { log('host is now ' + serverName); }) }); ================================================ FILE: examples/cluster-haproxy/server/public/style.css ================================================ /* Fix user-agent */ * { box-sizing: border-box; } html { font-weight: 300; -webkit-font-smoothing: antialiased; } html, input { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } html, body { height: 100%; margin: 0; padding: 0; } ul { list-style: none; word-wrap: break-word; } /* Pages */ .pages { height: 100%; margin: 0; padding: 0; width: 100%; } .page { height: 100%; position: absolute; width: 100%; } /* Login Page */ .login.page { background-color: #000; } .login.page .form { height: 100px; margin-top: -100px; position: absolute; text-align: center; top: 50%; width: 100%; } .login.page .form .usernameInput { background-color: transparent; border: none; border-bottom: 2px solid #fff; outline: none; padding-bottom: 15px; text-align: center; width: 400px; } .login.page .title { font-size: 200%; } .login.page .usernameInput { font-size: 200%; letter-spacing: 3px; } .login.page .title, .login.page .usernameInput { color: #fff; font-weight: 100; } /* Chat page */ .chat.page { display: none; } /* Font */ .messages { font-size: 150%; } .inputMessage { font-size: 100%; } .log { color: gray; font-size: 70%; margin: 5px; text-align: center; } /* Messages */ .chatArea { height: 100%; padding-bottom: 60px; } .messages { height: 100%; margin: 0; overflow-y: scroll; padding: 10px 20px 10px 20px; } .message.typing .messageBody { color: gray; } .username { font-weight: 700; overflow: hidden; padding-right: 15px; text-align: right; } /* Input */ .inputMessage { border: 10px solid #000; bottom: 0; height: 60px; left: 0; outline: none; padding-left: 10px; position: absolute; right: 0; width: 100%; } ================================================ FILE: examples/cluster-httpd/README.md ================================================ # Socket.IO Chat with httpd & redis A simple chat demo for socket.io ## How to use Install [Docker Compose](https://docs.docker.com/compose/install/), then: ``` $ docker-compose up -d ``` And then point your browser to `http://localhost:3000`. This will start four Socket.IO nodes, behind a httpd proxy which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](http://httpd.apache.org/docs/2.4/fr/mod/mod_proxy_balancer.html)). Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to. ``` # you can kill a given node, the client should reconnect to another node $ docker-compose stop server-george ``` ## Features - Multiple users can join a chat room by each entering a unique username on website load. - Users can type chat messages to the chat room. - A notification is sent to all users when a user joins or leaves the chatroom. ================================================ FILE: examples/cluster-httpd/docker-compose.yml ================================================ services: httpd: image: httpd:2.4-alpine volumes: - ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro links: - server-john - server-paul - server-george - server-ringo ports: - "3000:80" server-john: build: ./server links: - redis expose: - "3000" environment: - NAME=John server-paul: build: ./server links: - redis expose: - "3000" environment: - NAME=Paul server-george: build: ./server links: - redis expose: - "3000" environment: - NAME=George server-ringo: build: ./server links: - redis expose: - "3000" environment: - NAME=Ringo redis: image: redis:6 expose: - "6379" ================================================ FILE: examples/cluster-httpd/httpd.conf ================================================ Listen 80 ServerName localhost LoadModule mpm_event_module modules/mod_mpm_event.so LoadModule authn_file_module modules/mod_authn_file.so LoadModule authn_core_module modules/mod_authn_core.so LoadModule authz_host_module modules/mod_authz_host.so LoadModule authz_groupfile_module modules/mod_authz_groupfile.so LoadModule authz_user_module modules/mod_authz_user.so LoadModule authz_core_module modules/mod_authz_core.so LoadModule headers_module modules/mod_headers.so LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_balancer_module modules/mod_proxy_balancer.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so LoadModule rewrite_module modules/mod_rewrite.so LoadModule slotmem_shm_module modules/mod_slotmem_shm.so LoadModule unixd_module modules/mod_unixd.so User daemon Group daemon ErrorLog /proc/self/fd/2 Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED BalancerMember "http://server-john:3000" route=john BalancerMember "http://server-paul:3000" route=paul BalancerMember "http://server-george:3000" route=george BalancerMember "http://server-ringo:3000" route=ringo ProxySet stickysession=SERVERID BalancerMember "ws://server-john:3000" route=john BalancerMember "ws://server-paul:3000" route=paul BalancerMember "ws://server-george:3000" route=george BalancerMember "ws://server-ringo:3000" route=ringo ProxySet stickysession=SERVERID RewriteEngine On RewriteCond %{HTTP:Upgrade} =websocket [NC] RewriteRule /(.*) balancer://nodes_ws/$1 [P,L] RewriteCond %{HTTP:Upgrade} !=websocket [NC] RewriteRule /(.*) balancer://nodes_polling/$1 [P,L] # must be bigger than pingInterval (25s by default) + pingTimeout (20s by default) ProxyTimeout 60 ================================================ FILE: examples/cluster-httpd/server/Dockerfile ================================================ FROM node:14-alpine # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN npm install --prod # Bundle app source COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ] ================================================ FILE: examples/cluster-httpd/server/index.js ================================================ // Setup basic express server var express = require('express'); var app = express(); var server = require('http').createServer(app); var io = require('socket.io')(server); var redis = require('socket.io-redis'); var port = process.env.PORT || 3000; var serverName = process.env.NAME || 'Unknown'; io.adapter(redis({ host: 'redis', port: 6379 })); server.listen(port, function () { console.log('Server listening at port %d', port); console.log('Hello, I\'m %s, how can I help?', serverName); }); // Routing app.use(express.static(__dirname + '/public')); // Chatroom var numUsers = 0; io.on('connection', function (socket) { socket.emit('my-name-is', serverName); var addedUser = false; // when the client emits 'new message', this listens and executes socket.on('new message', function (data) { // we tell the client to execute 'new message' socket.broadcast.emit('new message', { username: socket.username, message: data }); }); // when the client emits 'add user', this listens and executes socket.on('add user', function (username) { if (addedUser) return; // we store the username in the socket session for this client socket.username = username; ++numUsers; addedUser = true; socket.emit('login', { numUsers: numUsers }); // echo globally (all clients) that a person has connected socket.broadcast.emit('user joined', { username: socket.username, numUsers: numUsers }); }); // when the client emits 'typing', we broadcast it to others socket.on('typing', function () { socket.broadcast.emit('typing', { username: socket.username }); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', function () { socket.broadcast.emit('stop typing', { username: socket.username }); }); // when the user disconnects.. perform this socket.on('disconnect', function () { if (addedUser) { --numUsers; // echo globally that this client has left socket.broadcast.emit('user left', { username: socket.username, numUsers: numUsers }); } }); }); ================================================ FILE: examples/cluster-httpd/server/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "BSD", "dependencies": { "express": "4.13.4", "socket.io": "^4.0.0", "socket.io-redis": "^6.0.1" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/cluster-httpd/server/public/index.html ================================================ Socket.IO Chat Example
        ================================================ FILE: examples/cluster-httpd/server/public/main.js ================================================ $(function() { var FADE_TIME = 150; // ms var TYPING_TIMER_LENGTH = 400; // ms var COLORS = [ '#e21400', '#91580f', '#f8a700', '#f78b00', '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', '#3b88eb', '#3824aa', '#a700ff', '#d300e7' ]; // Initialize variables var $window = $(window); var $usernameInput = $('.usernameInput'); // Input for username var $messages = $('.messages'); // Messages area var $inputMessage = $('.inputMessage'); // Input message input box var $loginPage = $('.login.page'); // The login page var $chatPage = $('.chat.page'); // The chatroom page // Prompt for setting a username var username; var connected = false; var typing = false; var lastTypingTime; var $currentInput = $usernameInput.focus(); var socket = io(); function addParticipantsMessage (data) { var message = ''; if (data.numUsers === 1) { message += "there's 1 participant"; } else { message += "there are " + data.numUsers + " participants"; } log(message); } // Sets the client's username function setUsername () { username = cleanInput($usernameInput.val().trim()); // If the username is valid if (username) { $loginPage.fadeOut(); $chatPage.show(); $loginPage.off('click'); $currentInput = $inputMessage.focus(); // Tell the server your username socket.emit('add user', username); } } // Sends a chat message function sendMessage () { var message = $inputMessage.val(); // Prevent markup from being injected into the message message = cleanInput(message); // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); addChatMessage({ username: username, message: message }); // tell server to execute 'new message' and send along one parameter socket.emit('new message', message); } } // Log a message function log (message, options) { var $el = $('
      • ').addClass('log').text(message); addMessageElement($el, options); } // Adds the visual chat message to the message list function addChatMessage (data, options) { // Don't fade the message in if there is an 'X was typing' var $typingMessages = getTypingMessages(data); options = options || {}; if ($typingMessages.length !== 0) { options.fade = false; $typingMessages.remove(); } var $usernameDiv = $('') .text(data.username) .css('color', getUsernameColor(data.username)); var $messageBodyDiv = $('') .text(data.message); var typingClass = data.typing ? 'typing' : ''; var $messageDiv = $('
      • ') .data('username', data.username) .addClass(typingClass) .append($usernameDiv, $messageBodyDiv); addMessageElement($messageDiv, options); } // Adds the visual chat typing message function addChatTyping (data) { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message function removeChatTyping (data) { getTypingMessages(data).fadeOut(function () { $(this).remove(); }); } // Adds a message element to the messages and scrolls to the bottom // el - The element to add as a message // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) function addMessageElement (el, options) { var $el = $(el); // Setup default options if (!options) { options = {}; } if (typeof options.fade === 'undefined') { options.fade = true; } if (typeof options.prepend === 'undefined') { options.prepend = false; } // Apply options if (options.fade) { $el.hide().fadeIn(FADE_TIME); } if (options.prepend) { $messages.prepend($el); } else { $messages.append($el); } $messages[0].scrollTop = $messages[0].scrollHeight; } // Prevents input from having injected markup function cleanInput (input) { return $('
        ').text(input).text(); } // Updates the typing event function updateTyping () { if (connected) { if (!typing) { typing = true; socket.emit('typing'); } lastTypingTime = (new Date()).getTime(); setTimeout(function () { var typingTimer = (new Date()).getTime(); var timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { socket.emit('stop typing'); typing = false; } }, TYPING_TIMER_LENGTH); } } // Gets the 'X is typing' messages of a user function getTypingMessages (data) { return $('.typing.message').filter(function (i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function function getUsernameColor (username) { // Compute hash code var hash = 7; for (var i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color var index = Math.abs(hash % COLORS.length); return COLORS[index]; } // Keyboard events $window.keydown(function (event) { // Auto-focus the current input when a key is typed if (!(event.ctrlKey || event.metaKey || event.altKey)) { $currentInput.focus(); } // When the client hits ENTER on their keyboard if (event.which === 13) { if (username) { sendMessage(); socket.emit('stop typing'); typing = false; } else { setUsername(); } } }); $inputMessage.on('input', function() { updateTyping(); }); // Click events // Focus input when clicking anywhere on login page $loginPage.click(function () { $currentInput.focus(); }); // Focus input when clicking on the message input's border $inputMessage.click(function () { $inputMessage.focus(); }); // Socket events // Whenever the server emits 'login', log the login message socket.on('login', function (data) { connected = true; // Display the welcome message var message = "Welcome to Socket.IO Chat – "; log(message, { prepend: true }); addParticipantsMessage(data); }); // Whenever the server emits 'new message', update the chat body socket.on('new message', function (data) { addChatMessage(data); }); // Whenever the server emits 'user joined', log it in the chat body socket.on('user joined', function (data) { log(data.username + ' joined'); addParticipantsMessage(data); }); // Whenever the server emits 'user left', log it in the chat body socket.on('user left', function (data) { log(data.username + ' left'); addParticipantsMessage(data); removeChatTyping(data); }); // Whenever the server emits 'typing', show the typing message socket.on('typing', function (data) { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message socket.on('stop typing', function (data) { removeChatTyping(data); }); socket.on('disconnect', function () { log('you have been disconnected'); }); socket.on('reconnect', function () { log('you have been reconnected'); if (username) { socket.emit('add user', username); } }); socket.on('reconnect_error', function () { log('attempt to reconnect has failed'); }); socket.on('my-name-is', function (serverName) { log('host is now ' + serverName); }) }); ================================================ FILE: examples/cluster-httpd/server/public/style.css ================================================ /* Fix user-agent */ * { box-sizing: border-box; } html { font-weight: 300; -webkit-font-smoothing: antialiased; } html, input { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } html, body { height: 100%; margin: 0; padding: 0; } ul { list-style: none; word-wrap: break-word; } /* Pages */ .pages { height: 100%; margin: 0; padding: 0; width: 100%; } .page { height: 100%; position: absolute; width: 100%; } /* Login Page */ .login.page { background-color: #000; } .login.page .form { height: 100px; margin-top: -100px; position: absolute; text-align: center; top: 50%; width: 100%; } .login.page .form .usernameInput { background-color: transparent; border: none; border-bottom: 2px solid #fff; outline: none; padding-bottom: 15px; text-align: center; width: 400px; } .login.page .title { font-size: 200%; } .login.page .usernameInput { font-size: 200%; letter-spacing: 3px; } .login.page .title, .login.page .usernameInput { color: #fff; font-weight: 100; } /* Chat page */ .chat.page { display: none; } /* Font */ .messages { font-size: 150%; } .inputMessage { font-size: 100%; } .log { color: gray; font-size: 70%; margin: 5px; text-align: center; } /* Messages */ .chatArea { height: 100%; padding-bottom: 60px; } .messages { height: 100%; margin: 0; overflow-y: scroll; padding: 10px 20px 10px 20px; } .message.typing .messageBody { color: gray; } .username { font-weight: 700; overflow: hidden; padding-right: 15px; text-align: right; } /* Input */ .inputMessage { border: 10px solid #000; bottom: 0; height: 60px; left: 0; outline: none; padding-left: 10px; position: absolute; right: 0; width: 100%; } ================================================ FILE: examples/cluster-nginx/README.md ================================================ # Socket.IO Chat with nginx & redis A simple chat demo for socket.io ## How to use Install [Docker Compose](https://docs.docker.com/compose/install/), then: ``` $ docker-compose up -d ``` And then point your browser to `http://localhost:3000`. This will start four Socket.IO nodes, behind a nginx proxy which will loadbalance the requests (using the IP of the client, see [ip_hash](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#ip_hash)). Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to. ``` # you can kill a given node, the client should reconnect to another node $ docker-compose stop server-george ``` A `client` container is included in the `docker-compose.yml` file, in order to test the routing. You can create additional `client` containers with: ``` $ docker-compose up -d --scale=client=10 client # and then $ docker-compose logs client ``` ## Features - Multiple users can join a chat room by each entering a unique username on website load. - Users can type chat messages to the chat room. - A notification is sent to all users when a user joins or leaves the chatroom. ================================================ FILE: examples/cluster-nginx/client/Dockerfile ================================================ FROM node:14-alpine # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN npm install --prod # Bundle app source COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ] ================================================ FILE: examples/cluster-nginx/client/index.js ================================================ const socket = require('socket.io-client')('ws://nginx'); socket.on('connect', () => { console.log('connected'); }); socket.on('my-name-is', (serverName) => { console.log(`connected to ${serverName}`); }); socket.on('disconnect', (reason) => { console.log(`disconnected due to ${reason}`); }); ================================================ FILE: examples/cluster-nginx/client/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "MIT", "dependencies": { "socket.io-client": "^4.0.0" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/cluster-nginx/docker-compose.yml ================================================ services: nginx: image: nginx:alpine volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro links: - server-john - server-paul - server-george - server-ringo ports: - "3000:80" server-john: build: ./server links: - redis expose: - "3000" environment: - NAME=John server-paul: build: ./server links: - redis expose: - "3000" environment: - NAME=Paul server-george: build: ./server links: - redis expose: - "3000" environment: - NAME=George server-ringo: build: ./server links: - redis expose: - "3000" environment: - NAME=Ringo client: build: ./client links: - nginx redis: image: redis:6 expose: - "6379" ================================================ FILE: examples/cluster-nginx/nginx.conf ================================================ # Reference: https://www.nginx.com/resources/wiki/start/topics/examples/full/ worker_processes 4; events { worker_connections 1024; } http { server { listen 80; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass http://nodes; # enable WebSockets proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } upstream nodes { # enable sticky session with either "hash" (uses the complete IP address) hash $remote_addr consistent; # or "ip_hash" (uses the first three octets of the client IPv4 address, or the entire IPv6 address) # ip_hash; # or "sticky" (needs commercial subscription) # sticky cookie srv_id expires=1h domain=.example.com path=/; server server-john:3000; server server-paul:3000; server server-george:3000; server server-ringo:3000; } } ================================================ FILE: examples/cluster-nginx/server/Dockerfile ================================================ FROM node:14-alpine # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN npm install --prod # Bundle app source COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ] ================================================ FILE: examples/cluster-nginx/server/index.js ================================================ const express = require('express'); const app = express(); const server = require('http').createServer(app); const io = require('socket.io')(server); const { createAdapter } = require('@socket.io/redis-adapter'); const { createClient } = require('redis'); const port = process.env.PORT || 3000; const serverName = process.env.NAME || 'Unknown'; const pubClient = createClient({ host: 'redis', port: 6379 }); const subClient = pubClient.duplicate(); io.adapter(createAdapter(pubClient, subClient)); server.listen(port, () => { console.log('Server listening at port %d', port); console.log('Hello, I\'m %s, how can I help?', serverName); }); // Routing app.use(express.static(__dirname + '/public')); // Chatroom let numUsers = 0; io.on('connection', socket => { socket.emit('my-name-is', serverName); let addedUser = false; // when the client emits 'new message', this listens and executes socket.on('new message', data => { // we tell the client to execute 'new message' socket.broadcast.emit('new message', { username: socket.username, message: data }); }); // when the client emits 'add user', this listens and executes socket.on('add user', username => { if (addedUser) return; // we store the username in the socket session for this client socket.username = username; ++numUsers; addedUser = true; socket.emit('login', { numUsers: numUsers }); // echo globally (all clients) that a person has connected socket.broadcast.emit('user joined', { username: socket.username, numUsers: numUsers }); }); // when the client emits 'typing', we broadcast it to others socket.on('typing', () => { socket.broadcast.emit('typing', { username: socket.username }); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', () => { socket.broadcast.emit('stop typing', { username: socket.username }); }); // when the user disconnects.. perform this socket.on('disconnect', () => { if (addedUser) { --numUsers; // echo globally that this client has left socket.broadcast.emit('user left', { username: socket.username, numUsers: numUsers }); } }); }); ================================================ FILE: examples/cluster-nginx/server/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "MIT", "dependencies": { "@socket.io/redis-adapter": "^7.0.1", "express": "4.13.4", "redis": "^3.1.2", "socket.io": "^4.0.0" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/cluster-nginx/server/public/index.html ================================================ Socket.IO Chat Example
          ================================================ FILE: examples/cluster-nginx/server/public/main.js ================================================ $(function() { var FADE_TIME = 150; // ms var TYPING_TIMER_LENGTH = 400; // ms var COLORS = [ '#e21400', '#91580f', '#f8a700', '#f78b00', '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', '#3b88eb', '#3824aa', '#a700ff', '#d300e7' ]; // Initialize variables var $window = $(window); var $usernameInput = $('.usernameInput'); // Input for username var $messages = $('.messages'); // Messages area var $inputMessage = $('.inputMessage'); // Input message input box var $loginPage = $('.login.page'); // The login page var $chatPage = $('.chat.page'); // The chatroom page // Prompt for setting a username var username; var connected = false; var typing = false; var lastTypingTime; var $currentInput = $usernameInput.focus(); var socket = io(); function addParticipantsMessage (data) { var message = ''; if (data.numUsers === 1) { message += "there's 1 participant"; } else { message += "there are " + data.numUsers + " participants"; } log(message); } // Sets the client's username function setUsername () { username = cleanInput($usernameInput.val().trim()); // If the username is valid if (username) { $loginPage.fadeOut(); $chatPage.show(); $loginPage.off('click'); $currentInput = $inputMessage.focus(); // Tell the server your username socket.emit('add user', username); } } // Sends a chat message function sendMessage () { var message = $inputMessage.val(); // Prevent markup from being injected into the message message = cleanInput(message); // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); addChatMessage({ username: username, message: message }); // tell server to execute 'new message' and send along one parameter socket.emit('new message', message); } } // Log a message function log (message, options) { var $el = $('
        • ').addClass('log').text(message); addMessageElement($el, options); } // Adds the visual chat message to the message list function addChatMessage (data, options) { // Don't fade the message in if there is an 'X was typing' var $typingMessages = getTypingMessages(data); options = options || {}; if ($typingMessages.length !== 0) { options.fade = false; $typingMessages.remove(); } var $usernameDiv = $('') .text(data.username) .css('color', getUsernameColor(data.username)); var $messageBodyDiv = $('') .text(data.message); var typingClass = data.typing ? 'typing' : ''; var $messageDiv = $('
        • ') .data('username', data.username) .addClass(typingClass) .append($usernameDiv, $messageBodyDiv); addMessageElement($messageDiv, options); } // Adds the visual chat typing message function addChatTyping (data) { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message function removeChatTyping (data) { getTypingMessages(data).fadeOut(function () { $(this).remove(); }); } // Adds a message element to the messages and scrolls to the bottom // el - The element to add as a message // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) function addMessageElement (el, options) { var $el = $(el); // Setup default options if (!options) { options = {}; } if (typeof options.fade === 'undefined') { options.fade = true; } if (typeof options.prepend === 'undefined') { options.prepend = false; } // Apply options if (options.fade) { $el.hide().fadeIn(FADE_TIME); } if (options.prepend) { $messages.prepend($el); } else { $messages.append($el); } $messages[0].scrollTop = $messages[0].scrollHeight; } // Prevents input from having injected markup function cleanInput (input) { return $('
          ').text(input).text(); } // Updates the typing event function updateTyping () { if (connected) { if (!typing) { typing = true; socket.emit('typing'); } lastTypingTime = (new Date()).getTime(); setTimeout(function () { var typingTimer = (new Date()).getTime(); var timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { socket.emit('stop typing'); typing = false; } }, TYPING_TIMER_LENGTH); } } // Gets the 'X is typing' messages of a user function getTypingMessages (data) { return $('.typing.message').filter(function (i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function function getUsernameColor (username) { // Compute hash code var hash = 7; for (var i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color var index = Math.abs(hash % COLORS.length); return COLORS[index]; } // Keyboard events $window.keydown(function (event) { // Auto-focus the current input when a key is typed if (!(event.ctrlKey || event.metaKey || event.altKey)) { $currentInput.focus(); } // When the client hits ENTER on their keyboard if (event.which === 13) { if (username) { sendMessage(); socket.emit('stop typing'); typing = false; } else { setUsername(); } } }); $inputMessage.on('input', function() { updateTyping(); }); // Click events // Focus input when clicking anywhere on login page $loginPage.click(function () { $currentInput.focus(); }); // Focus input when clicking on the message input's border $inputMessage.click(function () { $inputMessage.focus(); }); // Socket events // Whenever the server emits 'login', log the login message socket.on('login', function (data) { connected = true; // Display the welcome message var message = "Welcome to Socket.IO Chat – "; log(message, { prepend: true }); addParticipantsMessage(data); }); // Whenever the server emits 'new message', update the chat body socket.on('new message', function (data) { addChatMessage(data); }); // Whenever the server emits 'user joined', log it in the chat body socket.on('user joined', function (data) { log(data.username + ' joined'); addParticipantsMessage(data); }); // Whenever the server emits 'user left', log it in the chat body socket.on('user left', function (data) { log(data.username + ' left'); addParticipantsMessage(data); removeChatTyping(data); }); // Whenever the server emits 'typing', show the typing message socket.on('typing', function (data) { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message socket.on('stop typing', function (data) { removeChatTyping(data); }); socket.on('disconnect', function () { log('you have been disconnected'); }); socket.on('reconnect', function () { log('you have been reconnected'); if (username) { socket.emit('add user', username); } }); socket.on('reconnect_error', function () { log('attempt to reconnect has failed'); }); socket.on('my-name-is', function (serverName) { log('host is now ' + serverName); }) }); ================================================ FILE: examples/cluster-nginx/server/public/style.css ================================================ /* Fix user-agent */ * { box-sizing: border-box; } html { font-weight: 300; -webkit-font-smoothing: antialiased; } html, input { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } html, body { height: 100%; margin: 0; padding: 0; } ul { list-style: none; word-wrap: break-word; } /* Pages */ .pages { height: 100%; margin: 0; padding: 0; width: 100%; } .page { height: 100%; position: absolute; width: 100%; } /* Login Page */ .login.page { background-color: #000; } .login.page .form { height: 100px; margin-top: -100px; position: absolute; text-align: center; top: 50%; width: 100%; } .login.page .form .usernameInput { background-color: transparent; border: none; border-bottom: 2px solid #fff; outline: none; padding-bottom: 15px; text-align: center; width: 400px; } .login.page .title { font-size: 200%; } .login.page .usernameInput { font-size: 200%; letter-spacing: 3px; } .login.page .title, .login.page .usernameInput { color: #fff; font-weight: 100; } /* Chat page */ .chat.page { display: none; } /* Font */ .messages { font-size: 150%; } .inputMessage { font-size: 100%; } .log { color: gray; font-size: 70%; margin: 5px; text-align: center; } /* Messages */ .chatArea { height: 100%; padding-bottom: 60px; } .messages { height: 100%; margin: 0; overflow-y: scroll; padding: 10px 20px 10px 20px; } .message.typing .messageBody { color: gray; } .username { font-weight: 700; overflow: hidden; padding-right: 15px; text-align: right; } /* Input */ .inputMessage { border: 10px solid #000; bottom: 0; height: 60px; left: 0; outline: none; padding-left: 10px; position: absolute; right: 0; width: 100%; } ================================================ FILE: examples/cluster-traefik/README.md ================================================ # Socket.IO Chat with traefik & [redis](https://redis.io/) A simple chat demo for Socket.IO ## How to use Install [Docker Compose](https://docs.docker.com/compose/install/), then: ``` $ docker-compose up -d ``` And then point your browser to `http://localhost:3000`. You can then scale the server to multiple instances: ``` $ docker-compose up -d --scale=server=7 ``` The session stickiness, which is [required](https://socket.io/docs/v3/using-multiple-nodes/) when using multiple Socket.IO server instances, is achieved with a cookie. More information [here](https://doc.traefik.io/traefik/v2.0/routing/services/#sticky-sessions). ================================================ FILE: examples/cluster-traefik/docker-compose.yml ================================================ version: "3" services: traefik: image: traefik:2.4 volumes: - ./traefik.yml:/etc/traefik/traefik.yml - /var/run/docker.sock:/var/run/docker.sock links: - server ports: - "3000:80" - "8080:8080" server: build: ./server links: - redis labels: - "traefik.http.routers.chat.rule=PathPrefix(`/`)" - traefik.http.services.chat.loadBalancer.sticky.cookie.name=server_id - traefik.http.services.chat.loadBalancer.sticky.cookie.httpOnly=true redis: image: redis:6-alpine labels: - traefik.enable=false ================================================ FILE: examples/cluster-traefik/server/Dockerfile ================================================ FROM node:14-alpine # Create app directory RUN mkdir -p /usr/src/app WORKDIR /usr/src/app # Install app dependencies COPY package.json /usr/src/app/ RUN npm install --prod # Bundle app source COPY . /usr/src/app EXPOSE 3000 CMD [ "npm", "start" ] ================================================ FILE: examples/cluster-traefik/server/index.js ================================================ // Setup basic express server var express = require('express'); var app = express(); var server = require('http').createServer(app); var io = require('socket.io')(server); var redis = require('socket.io-redis'); var port = process.env.PORT || 3000; var crypto = require('crypto'); var serverName = crypto.randomBytes(3).toString('hex'); io.adapter(redis({ host: 'redis', port: 6379 })); server.listen(port, function () { console.log('Server listening at port %d', port); console.log('Hello, I\'m %s, how can I help?', serverName); }); // Routing app.use(express.static(__dirname + '/public')); // Chatroom var numUsers = 0; io.on('connection', function (socket) { socket.emit('my-name-is', serverName); var addedUser = false; // when the client emits 'new message', this listens and executes socket.on('new message', function (data) { // we tell the client to execute 'new message' socket.broadcast.emit('new message', { username: socket.username, message: data }); }); // when the client emits 'add user', this listens and executes socket.on('add user', function (username) { if (addedUser) return; // we store the username in the socket session for this client socket.username = username; ++numUsers; addedUser = true; socket.emit('login', { numUsers: numUsers }); // echo globally (all clients) that a person has connected socket.broadcast.emit('user joined', { username: socket.username, numUsers: numUsers }); }); // when the client emits 'typing', we broadcast it to others socket.on('typing', function () { socket.broadcast.emit('typing', { username: socket.username }); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', function () { socket.broadcast.emit('stop typing', { username: socket.username }); }); // when the user disconnects.. perform this socket.on('disconnect', function () { if (addedUser) { --numUsers; // echo globally that this client has left socket.broadcast.emit('user left', { username: socket.username, numUsers: numUsers }); } }); }); ================================================ FILE: examples/cluster-traefik/server/package.json ================================================ { "name": "socket.io-chat", "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", "author": "Grant Timmerman", "private": true, "license": "MIT", "dependencies": { "express": "4.13.4", "socket.io": "^4.0.0", "socket.io-redis": "^6.0.1" }, "scripts": { "start": "node index.js" } } ================================================ FILE: examples/cluster-traefik/server/public/index.html ================================================ Socket.IO Chat Example
            ================================================ FILE: examples/cluster-traefik/server/public/main.js ================================================ $(function() { var FADE_TIME = 150; // ms var TYPING_TIMER_LENGTH = 400; // ms var COLORS = [ '#e21400', '#91580f', '#f8a700', '#f78b00', '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', '#3b88eb', '#3824aa', '#a700ff', '#d300e7' ]; // Initialize variables var $window = $(window); var $usernameInput = $('.usernameInput'); // Input for username var $messages = $('.messages'); // Messages area var $inputMessage = $('.inputMessage'); // Input message input box var $loginPage = $('.login.page'); // The login page var $chatPage = $('.chat.page'); // The chatroom page // Prompt for setting a username var username; var connected = false; var typing = false; var lastTypingTime; var $currentInput = $usernameInput.focus(); var socket = io(); function addParticipantsMessage (data) { var message = ''; if (data.numUsers === 1) { message += "there's 1 participant"; } else { message += "there are " + data.numUsers + " participants"; } log(message); } // Sets the client's username function setUsername () { username = cleanInput($usernameInput.val().trim()); // If the username is valid if (username) { $loginPage.fadeOut(); $chatPage.show(); $loginPage.off('click'); $currentInput = $inputMessage.focus(); // Tell the server your username socket.emit('add user', username); } } // Sends a chat message function sendMessage () { var message = $inputMessage.val(); // Prevent markup from being injected into the message message = cleanInput(message); // if there is a non-empty message and a socket connection if (message && connected) { $inputMessage.val(''); addChatMessage({ username: username, message: message }); // tell server to execute 'new message' and send along one parameter socket.emit('new message', message); } } // Log a message function log (message, options) { var $el = $('
          • ').addClass('log').text(message); addMessageElement($el, options); } // Adds the visual chat message to the message list function addChatMessage (data, options) { // Don't fade the message in if there is an 'X was typing' var $typingMessages = getTypingMessages(data); options = options || {}; if ($typingMessages.length !== 0) { options.fade = false; $typingMessages.remove(); } var $usernameDiv = $('') .text(data.username) .css('color', getUsernameColor(data.username)); var $messageBodyDiv = $('') .text(data.message); var typingClass = data.typing ? 'typing' : ''; var $messageDiv = $('
          • ') .data('username', data.username) .addClass(typingClass) .append($usernameDiv, $messageBodyDiv); addMessageElement($messageDiv, options); } // Adds the visual chat typing message function addChatTyping (data) { data.typing = true; data.message = 'is typing'; addChatMessage(data); } // Removes the visual chat typing message function removeChatTyping (data) { getTypingMessages(data).fadeOut(function () { $(this).remove(); }); } // Adds a message element to the messages and scrolls to the bottom // el - The element to add as a message // options.fade - If the element should fade-in (default = true) // options.prepend - If the element should prepend // all other messages (default = false) function addMessageElement (el, options) { var $el = $(el); // Setup default options if (!options) { options = {}; } if (typeof options.fade === 'undefined') { options.fade = true; } if (typeof options.prepend === 'undefined') { options.prepend = false; } // Apply options if (options.fade) { $el.hide().fadeIn(FADE_TIME); } if (options.prepend) { $messages.prepend($el); } else { $messages.append($el); } $messages[0].scrollTop = $messages[0].scrollHeight; } // Prevents input from having injected markup function cleanInput (input) { return $('
            ').text(input).text(); } // Updates the typing event function updateTyping () { if (connected) { if (!typing) { typing = true; socket.emit('typing'); } lastTypingTime = (new Date()).getTime(); setTimeout(function () { var typingTimer = (new Date()).getTime(); var timeDiff = typingTimer - lastTypingTime; if (timeDiff >= TYPING_TIMER_LENGTH && typing) { socket.emit('stop typing'); typing = false; } }, TYPING_TIMER_LENGTH); } } // Gets the 'X is typing' messages of a user function getTypingMessages (data) { return $('.typing.message').filter(function (i) { return $(this).data('username') === data.username; }); } // Gets the color of a username through our hash function function getUsernameColor (username) { // Compute hash code var hash = 7; for (var i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + (hash << 5) - hash; } // Calculate color var index = Math.abs(hash % COLORS.length); return COLORS[index]; } // Keyboard events $window.keydown(function (event) { // Auto-focus the current input when a key is typed if (!(event.ctrlKey || event.metaKey || event.altKey)) { $currentInput.focus(); } // When the client hits ENTER on their keyboard if (event.which === 13) { if (username) { sendMessage(); socket.emit('stop typing'); typing = false; } else { setUsername(); } } }); $inputMessage.on('input', function() { updateTyping(); }); // Click events // Focus input when clicking anywhere on login page $loginPage.click(function () { $currentInput.focus(); }); // Focus input when clicking on the message input's border $inputMessage.click(function () { $inputMessage.focus(); }); // Socket events // Whenever the server emits 'login', log the login message socket.on('login', function (data) { connected = true; // Display the welcome message var message = "Welcome to Socket.IO Chat – "; log(message, { prepend: true }); addParticipantsMessage(data); }); // Whenever the server emits 'new message', update the chat body socket.on('new message', function (data) { addChatMessage(data); }); // Whenever the server emits 'user joined', log it in the chat body socket.on('user joined', function (data) { log(data.username + ' joined'); addParticipantsMessage(data); }); // Whenever the server emits 'user left', log it in the chat body socket.on('user left', function (data) { log(data.username + ' left'); addParticipantsMessage(data); removeChatTyping(data); }); // Whenever the server emits 'typing', show the typing message socket.on('typing', function (data) { addChatTyping(data); }); // Whenever the server emits 'stop typing', kill the typing message socket.on('stop typing', function (data) { removeChatTyping(data); }); socket.on('disconnect', function () { log('you have been disconnected'); }); socket.on('connect', function () { if (username) { log('you have been reconnected'); socket.emit('add user', username); } }); socket.io.on('reconnect_error', function () { log('attempt to reconnect has failed'); }); socket.on('my-name-is', function (serverName) { log('host is now ' + serverName); }) }); ================================================ FILE: examples/cluster-traefik/server/public/style.css ================================================ /* Fix user-agent */ * { box-sizing: border-box; } html { font-weight: 300; -webkit-font-smoothing: antialiased; } html, input { font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; } html, body { height: 100%; margin: 0; padding: 0; } ul { list-style: none; word-wrap: break-word; } /* Pages */ .pages { height: 100%; margin: 0; padding: 0; width: 100%; } .page { height: 100%; position: absolute; width: 100%; } /* Login Page */ .login.page { background-color: #000; } .login.page .form { height: 100px; margin-top: -100px; position: absolute; text-align: center; top: 50%; width: 100%; } .login.page .form .usernameInput { background-color: transparent; border: none; border-bottom: 2px solid #fff; outline: none; padding-bottom: 15px; text-align: center; width: 400px; } .login.page .title { font-size: 200%; } .login.page .usernameInput { font-size: 200%; letter-spacing: 3px; } .login.page .title, .login.page .usernameInput { color: #fff; font-weight: 100; } /* Chat page */ .chat.page { display: none; } /* Font */ .messages { font-size: 150%; } .inputMessage { font-size: 100%; } .log { color: gray; font-size: 70%; margin: 5px; text-align: center; } /* Messages */ .chatArea { height: 100%; padding-bottom: 60px; } .messages { height: 100%; margin: 0; overflow-y: scroll; padding: 10px 20px 10px 20px; } .message.typing .messageBody { color: gray; } .username { font-weight: 700; overflow: hidden; padding-right: 15px; text-align: right; } /* Input */ .inputMessage { border: 10px solid #000; bottom: 0; height: 60px; left: 0; outline: none; padding-left: 10px; position: absolute; right: 0; width: 100%; } ================================================ FILE: examples/cluster-traefik/traefik.yml ================================================ api: insecure: true entryPoints: web: address: ":80" providers: docker: {} ================================================ FILE: examples/connection-state-recovery-example/README.md ================================================ # Example with connection state recovery This example shows how to use the [Connection state recovery feature](https://socket.io/docs/v4/connection-state-recovery). ![Video of the example](assets/csr.gif) ## How to use ```shell # choose your module syntax (either ES modules or CommonJS) $ cd esm/ # install the dependencies $ npm i # start the server $ node index.js ``` And point your browser to `http://localhost:3000`. You can also run this example directly in your browser on: - [CodeSandbox](https://codesandbox.io/p/sandbox/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js) - [StackBlitz](https://stackblitz.com/github/socketio/socket.io/tree/main/examples/connection-state-recovery-example/esm?file=index.js) ================================================ FILE: examples/connection-state-recovery-example/cjs/.codesandbox/Dockerfile ================================================ FROM node:20-bullseye ================================================ FILE: examples/connection-state-recovery-example/cjs/.codesandbox/tasks.json ================================================ { // These tasks will run in order when initializing your CodeSandbox project. "setupTasks": [ { "name": "Install Dependencies", "command": "npm install" } ], // These tasks can be run from CodeSandbox. Running one will open a log in the app. "tasks": { "npm start": { "name": "npm start", "command": "npm start", "runAtStart": true } } } ================================================ FILE: examples/connection-state-recovery-example/cjs/index.html ================================================ Connection state recovery | Socket.IO

            Status: disconnected

            Recovered? -

            Latest messages:

              ================================================ FILE: examples/connection-state-recovery-example/cjs/index.js ================================================ const { readFile } = require("node:fs/promises"); const { createServer } = require("node:http"); const { Server } = require("socket.io"); const httpServer = createServer(async (req, res) => { if (req.url !== "/") { res.writeHead(404); res.end("Not found"); return; } // reload the file every time const content = await readFile("index.html"); const length = Buffer.byteLength(content); res.writeHead(200, { "Content-Type": "text/html", "Content-Length": length, }); res.end(content); }); const io = new Server(httpServer, { connectionStateRecovery: { // the backup duration of the sessions and the packets maxDisconnectionDuration: 2 * 60 * 1000, // whether to skip middlewares upon successful recovery skipMiddlewares: true, }, }); io.on("connection", (socket) => { console.log(`connect ${socket.id}`); if (socket.recovered) { console.log("recovered!"); console.log("socket.rooms:", socket.rooms); console.log("socket.data:", socket.data); } else { console.log("new connection"); socket.join("sample room"); socket.data.foo = "bar"; } socket.on("disconnect", (reason) => { console.log(`disconnect ${socket.id} due to ${reason}`); }); }); setInterval(() => { io.emit("ping", new Date().toISOString()); }, 1000); httpServer.listen(3000); ================================================ FILE: examples/connection-state-recovery-example/cjs/package.json ================================================ { "name": "connection-state-recovery-example", "version": "0.0.1", "private": true, "type": "commonjs", "description": "Example with connection state recovery", "scripts": { "start": "node index.js" }, "dependencies": { "socket.io": "^4.7.2" } } ================================================ FILE: examples/connection-state-recovery-example/esm/.codesandbox/Dockerfile ================================================ FROM node:20-bullseye ================================================ FILE: examples/connection-state-recovery-example/esm/.codesandbox/tasks.json ================================================ { // These tasks will run in order when initializing your CodeSandbox project. "setupTasks": [ { "name": "Install Dependencies", "command": "npm install" } ], // These tasks can be run from CodeSandbox. Running one will open a log in the app. "tasks": { "npm start": { "name": "npm start", "command": "npm start", "runAtStart": true } } } ================================================ FILE: examples/connection-state-recovery-example/esm/index.html ================================================ Connection state recovery | Socket.IO

              Status: disconnected

              Recovered? -

              Latest messages:

                ================================================ FILE: examples/connection-state-recovery-example/esm/index.js ================================================ import { readFile } from "node:fs/promises"; import { createServer } from "node:http"; import { Server } from "socket.io"; const httpServer = createServer(async (req, res) => { if (req.url !== "/") { res.writeHead(404); res.end("Not found"); return; } // reload the file every time const content = await readFile("index.html"); const length = Buffer.byteLength(content); res.writeHead(200, { "Content-Type": "text/html", "Content-Length": length, }); res.end(content); }); const io = new Server(httpServer, { connectionStateRecovery: { // the backup duration of the sessions and the packets maxDisconnectionDuration: 2 * 60 * 1000, // whether to skip middlewares upon successful recovery skipMiddlewares: true, }, }); io.on("connection", (socket) => { console.log(`connect ${socket.id}`); if (socket.recovered) { console.log("recovered!"); console.log("socket.rooms:", socket.rooms); console.log("socket.data:", socket.data); } else { console.log("new connection"); socket.join("sample room"); socket.data.foo = "bar"; } socket.on("disconnect", (reason) => { console.log(`disconnect ${socket.id} due to ${reason}`); }); }); setInterval(() => { io.emit("ping", new Date().toISOString()); }, 1000); httpServer.listen(3000); ================================================ FILE: examples/connection-state-recovery-example/esm/package.json ================================================ { "name": "connection-state-recovery-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with connection state recovery", "scripts": { "start": "node index.js" }, "dependencies": { "socket.io": "^4.7.2" } } ================================================ FILE: examples/create-react-app-example/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/create-react-app-example/README.md ================================================ This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts In the project directory, you can run: ### `yarn start` Runs the app in the development mode.
                Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.
                You will also see any lint errors in the console. ### `yarn start-server` Starts the Socket.IO server. ### `yarn test` Launches the test runner in the interactive watch mode.
                See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `yarn build` Builds the app for production to the `build` folder.
                It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.
                Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### `yarn eject` **Note: this is a one-way operation. Once you `eject`, you can’t go back!** If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). ### Code Splitting This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting ### Analyzing the Bundle Size This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size ### Making a Progressive Web App This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app ### Advanced Configuration This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration ### Deployment This section has moved here: https://facebook.github.io/create-react-app/docs/deployment ### `yarn build` fails to minify This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify ================================================ FILE: examples/create-react-app-example/package.json ================================================ { "name": "create-react-app-example", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "^5.0.1", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" }, "scripts": { "start": "react-scripts start", "start-server": "node server.js", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: examples/create-react-app-example/public/index.html ================================================ React App
                ================================================ FILE: examples/create-react-app-example/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: examples/create-react-app-example/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: examples/create-react-app-example/server.js ================================================ const io = require('socket.io')({ cors: { origin: ['http://localhost:3000'] } }); io.on('connection', socket => { console.log(`connect: ${socket.id}`); socket.on('hello!', () => { console.log(`hello from ${socket.id}`); }); socket.on('disconnect', () => { console.log(`disconnect: ${socket.id}`); }); }); io.listen(3001); setInterval(() => { io.emit('message', new Date().toISOString()); }, 1000); ================================================ FILE: examples/create-react-app-example/src/App.css ================================================ .App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: examples/create-react-app-example/src/App.js ================================================ import React, { useState, useEffect } from 'react'; import logo from './logo.svg'; import './App.css'; import io from 'socket.io-client'; const socket = io('localhost:3001'); function App() { const [isConnected, setIsConnected] = useState(socket.connected); const [lastMessage, setLastMessage] = useState(null); useEffect(() => { socket.on('connect', () => { setIsConnected(true); }); socket.on('disconnect', () => { setIsConnected(false); }); socket.on('message', data => { setLastMessage(data); }); return () => { socket.off('connect'); socket.off('disconnect'); socket.off('message'); }; }, []); const sendMessage = () => { socket.emit('hello!'); } return (
                logo

                Connected: { '' + isConnected }

                Last message: { lastMessage || '-' }

                Edit src/App.js and save to reload.

                Learn React
                ); } export default App; ================================================ FILE: examples/create-react-app-example/src/App.test.js ================================================ import React from 'react'; import { render } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { const { getByText } = render(); const linkElement = getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: examples/create-react-app-example/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: examples/create-react-app-example/src/index.js ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; const container = document.getElementById('root'); const root = createRoot(container) root.render( ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister(); ================================================ FILE: examples/create-react-app-example/src/serviceWorker.js ================================================ // This optional code is used to register a service worker. // register() is not called by default. // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) // will only see deployed updates on subsequent visits to a page, after all the // existing tabs open on the page have been closed, since previously cached // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.0/8 are considered localhost for IPv4. window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); export function register(config) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to // serve assets; see https://github.com/facebook/create-react-app/issues/2374 return; } window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Let's check if a service worker still exists or not. checkValidServiceWorker(swUrl, config); // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA' ); }); } else { // Is not localhost. Just register service worker registerValidSW(swUrl, config); } }); } } function registerValidSW(swUrl, config) { navigator.serviceWorker .register(swUrl) .then(registration => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { return; } installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' ); // Execute callback if (config && config.onUpdate) { config.onUpdate(registration); } } else { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. console.log('Content is cached for offline use.'); // Execute callback if (config && config.onSuccess) { config.onSuccess(registration); } } } }; }; }) .catch(error => { console.error('Error during service worker registration:', error); }); } function checkValidServiceWorker(swUrl, config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl, { headers: { 'Service-Worker': 'script' }, }) .then(response => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type'); if ( response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { window.location.reload(); }); }); } else { // Service worker found. Proceed as normal. registerValidSW(swUrl, config); } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.' ); }); } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready .then(registration => { registration.unregister(); }) .catch(error => { console.error(error.message); }); } } ================================================ FILE: examples/create-react-app-example/src/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect'; ================================================ FILE: examples/custom-parsers/README.md ================================================ # Socket.IO custom parsers Since Socket.IO version 2.0.0, you can provide your custom parser, according to the needs of your application. Several parsers are showcased here: - the default one: [socket.io-parser](https://github.com/socketio/socket.io-parser) - one based on msgpack: [socket.io-msgpack-parser](https://github.com/darrachequesne/socket.io-msgpack-parser) - one based on native JSON: [socket.io-json-parser](https://github.com/darrachequesne/socket.io-json-parser) - a custom one based on [schemapack](https://github.com/phretaddin/schemapack) They are tested with various payloads: - string: `['1', '2', ... '1000']` - numeric: `[1, 2, ... 1000]` - binary: `Buffer.allocUnsafe(1000), where buf[i] = i` ## How to use ``` $ npm i && npm start ``` ## Results | bytes / packet | CONNECT packet | string | numeric | binary | |----------------|----------------|--------|---------|-----------| | default | 1 | 5903 | 3904 | 43 + 1000 | | msgpack | 20 | 3919 | 2646 | 1029 | | JSON | 20 | 5930 | 3931 | 3625 | | schemapack | 20 | 3895 | 2005 | 1005 | ## Comparison `default parser` - supports any serializable datastructure, including Blob and File - **but** binary payload is encoded as 2 packets `msgpack` - the size of payloads containing mostly numeric values will be greatly reduced - **but** rely on [ArrayBuffer](https://caniuse.com/#feat=typedarrays) in the browser (IE > 9) `JSON` - optimized - **but** does not support binary payloads `schemapack` - the most efficient in both speed and size - **but** you have to provide a schema for each packet ================================================ FILE: examples/custom-parsers/package.json ================================================ { "name": "parsers", "version": "1.0.0", "description": "Various socket.io parsers", "scripts": { "build": "webpack --config ./support/webpack.config.js", "start": "npm run build && node ./src/server.js" }, "author": "Damien Arrachequesne", "license": "MIT", "dependencies": { "component-emitter": "^1.2.1", "express": "^4.15.2", "schemapack": "^1.4.2", "socket.io": "^4.0.0", "socket.io-client": "^4.0.0", "socket.io-json-parser": "^3.0.0", "socket.io-msgpack-parser": "^3.0.1", "webpack": "^2.4.1" } } ================================================ FILE: examples/custom-parsers/public/.gitignore ================================================ *.bundle.js ================================================ FILE: examples/custom-parsers/public/index.html ================================================ Socket.IO custom parsers ================================================ FILE: examples/custom-parsers/src/client1.js ================================================ const socket = require('socket.io-client')('localhost:3001', {}); socket.io.engine.on('data', (data) => console.log('[default]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength))); socket.on('string', (data) => console.log('[default] [string]', data)); socket.on('numeric', (data) => console.log('[default] [numeric]', data)); socket.on('binary', (data) => console.log('[default] [binary]', data)); ================================================ FILE: examples/custom-parsers/src/client2.js ================================================ const customParser = require('socket.io-msgpack-parser'); const socket = require('socket.io-client')('http://localhost:3002', { parser: customParser }); socket.io.engine.on('data', (data) => console.log('[msgpack]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength))); socket.on('string', (data) => console.log('[msgpack] [string]', data)); socket.on('numeric', (data) => console.log('[msgpack] [numeric]', data)); socket.on('binary', (data) => console.log('[msgpack] [binary]', data)); ================================================ FILE: examples/custom-parsers/src/client3.js ================================================ const customParser = require('socket.io-json-parser'); const socket = require('socket.io-client')('localhost:3003', { parser: customParser }); socket.io.engine.on('data', (data) => console.log('[json]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength))); socket.on('string', (data) => console.log('[json] [string]', data)); socket.on('numeric', (data) => console.log('[json] [numeric]', data)); socket.on('binary', (data) => console.log('[json] [binary]', data)); ================================================ FILE: examples/custom-parsers/src/client4.js ================================================ const customParser = require('./custom-parser'); const socket = require('socket.io-client')('localhost:3004', { parser: customParser }); socket.io.engine.on('data', (data) => console.log('[custom]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength))); socket.on('string', (data) => console.log('[custom] [string]', data)); socket.on('numeric', (data) => console.log('[custom] [numeric]', data)); socket.on('binary', (data) => console.log('[custom] [binary]', data)); ================================================ FILE: examples/custom-parsers/src/custom-parser.js ================================================ const Emitter = require('component-emitter'); const schemapack = require('schemapack'); /** * Packet types (see https://github.com/socketio/socket.io-protocol) */ const TYPES = { CONNECT: 0, DISCONNECT: 1, EVENT: 2, ACK: 3, ERROR: 4, BINARY_EVENT: 5, BINARY_ACK: 6 }; const stringSchema = schemapack.build({ _id: 'uint8', data: [ 'string' ], nsp: 'string' }); const numericSchema = schemapack.build({ _id: 'uint8', data: [ 'uint16' ], nsp: 'string' }); const binarySchema = schemapack.build({ _id: 'uint8', data: 'buffer', nsp: 'string' }); const errorPacket = { type: TYPES.ERROR, data: 'parser error' }; class Encoder { encode (packet) { switch (packet.type) { case TYPES.EVENT: return [ this.pack(packet) ]; default: return [ JSON.stringify(packet) ]; } } pack (packet) { let eventName = packet.data[0]; let flatPacket = { data: packet.data[1], nsp: packet.nsp }; switch (eventName) { case 'string': flatPacket._id = 1; return stringSchema.encode(flatPacket); case 'numeric': flatPacket._id = 2; return numericSchema.encode(flatPacket); case 'binary': flatPacket._id = 3; return binarySchema.encode(flatPacket); default: throw new Error('unknown event name: ' + eventName); } } } class Decoder extends Emitter { add (obj) { if (typeof obj === 'string') { this.parseJSON(obj); } else { this.parseBinary(obj); } } parseJSON (obj) { try { let decoded = JSON.parse(obj); this.emit('decoded', decoded); } catch (e) { this.emit('decoded', errorPacket); } } parseBinary (obj) { let view = new Uint8Array(obj); let packetId = view[0]; try { let packet = { type: TYPES.EVENT }; let decoded; switch (packetId) { case 1: decoded = stringSchema.decode(obj); packet.data = [ 'string', decoded.data ]; packet.nsp = decoded.nsp; break; case 2: decoded = numericSchema.decode(obj); packet.data = [ 'numeric', decoded.data ]; packet.nsp = decoded.nsp; break; case 3: decoded = binarySchema.decode(obj); packet.data = [ 'binary', decoded.data.buffer ]; packet.nsp = decoded.nsp; break; default: throw new Error('unknown type'); } this.emit('decoded', packet); } catch (e) { this.emit('decoded', errorPacket); } } destroy () {} } exports.Encoder = Encoder; exports.Decoder = Decoder; ================================================ FILE: examples/custom-parsers/src/server.js ================================================ const express = require('express'); const app = express(); const server = require('http').createServer(app); const path = require('path'); const port = process.env.PORT || 3000; app.use(express.static(path.join(__dirname, '../public'))); server.listen(port, () => console.log('>>> http://localhost:' + port)); const io = require('socket.io'); const msgpackParser = require('socket.io-msgpack-parser'); const jsonParser = require('socket.io-json-parser'); const customParser = require('./custom-parser'); const cors = { origin: ["http://localhost:3000"] } let server1 = io(3001, { cors }); let server2 = io(3002, { parser: msgpackParser, cors }); let server3 = io(3003, { parser: jsonParser, cors }); let server4 = io(3004, { parser: customParser, cors }); let string = []; let numeric = []; let binary = Buffer.allocUnsafe(1e3); for (var i = 0; i < 1e3; i++) { string.push('' + i); numeric.push(i); binary[i] = i; } server1.on('connect', onConnect(1000)); server2.on('connect', onConnect(2000)); server3.on('connect', onConnect(3000)); server4.on('connect', onConnect(4000)); function onConnect (delay) { return function (socket) { console.log('connect ' + socket.id); setTimeout(() => { socket.emit('string', string); socket.emit('numeric', numeric); socket.emit('binary', binary); }, delay); socket.on('disconnect', () => console.log('disconnect ' + socket.id)); }; } ================================================ FILE: examples/custom-parsers/support/webpack.config.js ================================================ const path = require('path'); module.exports = { entry: { client1: './src/client1.js', client2: './src/client2.js', client3: './src/client3.js', client4: './src/client4.js' }, output: { path: path.resolve(__dirname, '../public'), filename: '[name].bundle.js' } }; ================================================ FILE: examples/es-modules/README.md ================================================ # Example with [ES modules](https://nodejs.org/api/esm.html) ## How to use ``` # install the dependencies $ npm ci # start the server $ node server.js # start the client $ node client.js ``` You need Node.js `>=12.17.0`. ================================================ FILE: examples/es-modules/client.js ================================================ import { Manager } from "socket.io-client"; const manager = new Manager("ws://localhost:8080"); const socket = manager.socket("/"); socket.on("connect", () => { console.log(`connect ${socket.id}`); }); socket.on("disconnect", () => { console.log(`disconnect`); }); setInterval(() => { socket.emit("ping", () => { console.log("pong"); }); }, 1000); ================================================ FILE: examples/es-modules/package.json ================================================ { "name": "es-modules", "version": "1.0.0", "description": "An example with ES modules (https://nodejs.org/api/esm.html)", "type": "module", "author": "Damien Arrachequesne", "license": "MIT", "engines": { "node": ">=12.17.0" }, "dependencies": { "socket.io": "^4.0.0", "socket.io-client": "^4.0.0" } } ================================================ FILE: examples/es-modules/server.js ================================================ import { Server } from "socket.io"; const io = new Server(8080); io.on("connection", (socket) => { console.log(`connect ${socket.id}`); socket.on("ping", (cb) => { console.log("ping"); cb(); }); socket.on("disconnect", () => { console.log(`disconnect ${socket.id}`); }); }); ================================================ FILE: examples/expo-example/.gitignore ================================================ # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files # dependencies node_modules/ # Expo .expo/ dist/ web-build/ # Native *.orig.* *.jks *.p8 *.p12 *.key *.mobileprovision # Metro .metro-health-check* # debug npm-debug.* yarn-debug.* yarn-error.* # macOS .DS_Store *.pem # local env files .env*.local # typescript *.tsbuildinfo ================================================ FILE: examples/expo-example/App.js ================================================ import { StyleSheet, Text, View } from 'react-native'; import { socket } from './socket'; import { useEffect, useState } from 'react'; export default function App() { const [isConnected, setIsConnected] = useState(false); const [transport, setTransport] = useState('N/A'); useEffect(() => { if (socket.connected) { onConnect(); } function onConnect() { setIsConnected(true); setTransport(socket.io.engine.transport.name); socket.io.engine.on('upgrade', (transport) => { setTransport(transport.name); }); } function onDisconnect() { setIsConnected(false); setTransport('N/A'); } socket.on('connect', onConnect); socket.on('disconnect', onDisconnect); return () => { socket.off('connect', onConnect); socket.off('disconnect', onDisconnect); }; }, []); return ( Status: { isConnected ? 'connected' : 'disconnected' } Transport: { transport } ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); ================================================ FILE: examples/expo-example/app.json ================================================ { "expo": { "name": "expo-example", "slug": "expo-example", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "web": { "favicon": "./assets/favicon.png" } } } ================================================ FILE: examples/expo-example/babel.config.js ================================================ module.exports = function(api) { api.cache(true); return { presets: ['babel-preset-expo'], }; }; ================================================ FILE: examples/expo-example/package.json ================================================ { "name": "expo-example", "version": "1.0.0", "main": "node_modules/expo/AppEntry.js", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web" }, "dependencies": { "@expo/metro-runtime": "~3.1.3", "expo": "~50.0.14", "expo-status-bar": "~1.11.1", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.73.6", "react-native-web": "~0.19.6", "socket.io-client": "^4.7.5" }, "devDependencies": { "@babel/core": "^7.20.0" }, "private": true } ================================================ FILE: examples/expo-example/server/index.js ================================================ import { Server } from 'socket.io'; const io = new Server({ cors: { origin: ['http://localhost:8081'] } }); io.on('connection', (socket) => { console.log(`connect: ${socket.id}`, socket.request.headers); socket.on('disconnect', () => { console.log(`disconnect: ${socket.id}`); }); }); io.listen(3000); ================================================ FILE: examples/expo-example/server/package.json ================================================ { "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "start": "node index.js" }, "author": "", "license": "ISC", "dependencies": { "socket.io": "^4.7.5" } } ================================================ FILE: examples/expo-example/socket.js ================================================ import { io } from 'socket.io-client'; export const socket = io('http://192.168.0.10:3000'); // use the IP address of your machine ================================================ FILE: examples/express-session-example/README.md ================================================ # Example with [express-session](https://www.npmjs.com/package/express-session) This example shows how to share a session context between [Express](http://expressjs.com/) and [Socket.IO](https://socket.io/docs/v4/): ![Video of the example](assets/demo.gif) Please read the related guide: https://socket.io/how-to/use-with-express-session ## How to use ``` $ npm install $ npm start ``` And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable. ================================================ FILE: examples/express-session-example/cjs/index.html ================================================ Example with express-session

                Count: 0

                Status: disconnected

                Count: 0

                ================================================ FILE: examples/express-session-example/cjs/index.js ================================================ const express = require("express"); const { createServer } = require("node:http"); const { join } = require("node:path"); const { Server } = require("socket.io"); const session = require("express-session"); const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.get("/", (req, res) => { res.sendFile(join(__dirname, "index.html")); }); app.post("/incr", (req, res) => { const session = req.session; session.count = (session.count || 0) + 1; res.status(200).end("" + session.count); io.to(session.id).emit("current count", session.count); }); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(sessionId).disconnectSockets(); res.status(204).end(); }); }); const io = new Server(httpServer); io.engine.use(sessionMiddleware); io.on("connection", (socket) => { const req = socket.request; socket.join(req.session.id); socket.on("incr", (cb) => { req.session.reload((err) => { if (err) { // session has expired return socket.disconnect(); } req.session.count = (req.session.count || 0) + 1; req.session.save(() => { cb(req.session.count); }); }); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/express-session-example/cjs/package.json ================================================ { "name": "express-session-example", "version": "0.0.1", "private": true, "type": "commonjs", "description": "Example with express-session (https://github.com/expressjs/session)", "scripts": { "start": "node index.js" }, "dependencies": { "express": "~4.17.3", "express-session": "~1.17.2", "socket.io": "^4.7.2" } } ================================================ FILE: examples/express-session-example/esm/index.html ================================================ Example with express-session

                Count: 0

                Status: disconnected

                Count: 0

                ================================================ FILE: examples/express-session-example/esm/index.js ================================================ import express from "express"; import { createServer } from "node:http"; import { Server } from "socket.io"; import session from "express-session"; const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.get("/", (req, res) => { res.sendFile(new URL("./index.html", import.meta.url).pathname); }); app.post("/incr", (req, res) => { const session = req.session; session.count = (session.count || 0) + 1; res.status(200).end("" + session.count); io.to(session.id).emit("current count", session.count); }); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(sessionId).disconnectSockets(); res.status(204).end(); }); }); const io = new Server(httpServer); io.engine.use(sessionMiddleware); io.on("connection", (socket) => { const req = socket.request; socket.join(req.session.id); socket.on("incr", (cb) => { req.session.reload((err) => { if (err) { // session has expired return socket.disconnect(); } req.session.count = (req.session.count || 0) + 1; req.session.save(() => { cb(req.session.count); }); }); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/express-session-example/esm/package.json ================================================ { "name": "express-session-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with express-session (https://github.com/expressjs/session)", "scripts": { "start": "node index.js" }, "dependencies": { "express": "~4.17.3", "express-session": "~1.17.2", "socket.io": "^4.7.2" } } ================================================ FILE: examples/express-session-example/ts/index.html ================================================ Example with express-session

                Count: 0

                Status: disconnected

                Count: 0

                ================================================ FILE: examples/express-session-example/ts/index.ts ================================================ import express = require("express"); import { createServer } from "http"; import { Server } from "socket.io"; import session from "express-session"; import { type Request } from "express"; declare module "express-session" { interface SessionData { count: number; } } const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.get("/", (req, res) => { res.sendFile(new URL("./index.html", import.meta.url).pathname); }); app.post("/incr", (req, res) => { const session = req.session; session.count = (session.count || 0) + 1; res.status(200).end("" + session.count); io.to(session.id).emit("current count", session.count); }); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(sessionId).disconnectSockets(); res.status(204).end(); }); }); const io = new Server(httpServer); io.engine.use(sessionMiddleware); io.on("connection", (socket) => { const req = socket.request as Request; socket.join(req.session.id); socket.on("incr", (cb) => { req.session.reload((err) => { if (err) { // session has expired return socket.disconnect(); } req.session.count = (req.session.count || 0) + 1; req.session.save(() => { cb(req.session.count); }); }); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/express-session-example/ts/package.json ================================================ { "name": "express-session-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with express-session (https://github.com/expressjs/session)", "scripts": { "start": "ts-node index.ts" }, "dependencies": { "@types/express": "^4.17.17", "@types/express-session": "^1.17.7", "@types/node": "^20.6.0", "express": "~4.17.3", "express-session": "~1.17.2", "socket.io": "^4.7.2", "ts-node": "^10.9.1", "typescript": "^5.2.2" } } ================================================ FILE: examples/express-session-example/ts/tsconfig.json ================================================ { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "strict": true }, "ts-node": { "esm": true } } ================================================ FILE: examples/nestjs-example/.eslintrc.js ================================================ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { project: 'tsconfig.json', tsconfigRootDir: __dirname, sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], root: true, env: { node: true, jest: true, }, ignorePatterns: ['.eslintrc.js'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }; ================================================ FILE: examples/nestjs-example/.gitignore ================================================ # compiled output /dist /node_modules /build # Logs logs *.log npm-debug.log* pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # OS .DS_Store # Tests /coverage /.nyc_output # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # temp directory .temp .tmp # Runtime data pids *.pid *.seed *.pid.lock # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json ================================================ FILE: examples/nestjs-example/.prettierrc ================================================ { "singleQuote": true, "trailingComma": "all" } ================================================ FILE: examples/nestjs-example/README.md ================================================

                Nest Logo

                [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 [circleci-url]: https://circleci.com/gh/nestjs/nest

                A progressive Node.js framework for building efficient and scalable server-side applications.

                NPM Version Package License NPM Downloads CircleCI Coverage Discord Backers on Open Collective Sponsors on Open Collective Support us

                ## Description [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. ## Installation ```bash $ npm install ``` ## Running the app ```bash # development $ npm run start # watch mode $ npm run start:dev # production mode $ npm run start:prod ``` ## Test ```bash # unit tests $ npm run test # e2e tests $ npm run test:e2e # test coverage $ npm run test:cov ``` ## Support Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). ## Stay in touch - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) - Website - [https://nestjs.com](https://nestjs.com/) - Twitter - [@nestframework](https://twitter.com/nestframework) ## License Nest is [MIT licensed](LICENSE). ================================================ FILE: examples/nestjs-example/nest-cli.json ================================================ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true } } ================================================ FILE: examples/nestjs-example/package.json ================================================ { "name": "nestjs-example", "version": "0.0.1", "description": "", "author": "", "private": true, "license": "UNLICENSED", "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-socket.io": "^10.3.10", "@nestjs/websockets": "^10.3.10", "hbs": "^4.2.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" }, "jest": { "moduleFileExtensions": [ "js", "json", "ts" ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", "testEnvironment": "node" } } ================================================ FILE: examples/nestjs-example/src/app.controller.spec.ts ================================================ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; describe('AppController', () => { let appController: AppController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], providers: [AppService], }).compile(); appController = app.get(AppController); }); describe('root', () => { it('should return "Hello World!"', () => { expect(appController.getHello()).toBe('Hello World!'); }); }); }); ================================================ FILE: examples/nestjs-example/src/app.controller.ts ================================================ import { Controller, Get, Render } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() @Render('index') root() { return { message: 'Hello world2!' }; } } ================================================ FILE: examples/nestjs-example/src/app.module.ts ================================================ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { EventsModule } from './events/events.module'; @Module({ imports: [EventsModule], controllers: [AppController], providers: [AppService], }) export class AppModule {} ================================================ FILE: examples/nestjs-example/src/app.service.ts ================================================ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } ================================================ FILE: examples/nestjs-example/src/events/events.gateway.ts ================================================ import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server } from 'socket.io'; @WebSocketGateway({}) export class EventsGateway { @WebSocketServer() io: Server; @SubscribeMessage('hello') handleEvent(@MessageBody() data: string): string { return data.split('').reverse().join(''); } } ================================================ FILE: examples/nestjs-example/src/events/events.module.ts ================================================ import { Module } from '@nestjs/common'; import { EventsGateway } from './events.gateway'; @Module({ providers: [EventsGateway], }) export class EventsModule {} ================================================ FILE: examples/nestjs-example/src/main.ts ================================================ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'node:path'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('hbs'); await app.listen(3000); } bootstrap(); ================================================ FILE: examples/nestjs-example/test/app.e2e-spec.ts ================================================ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') .expect(200) .expect('Hello World!'); }); }); ================================================ FILE: examples/nestjs-example/test/jest-e2e.json ================================================ { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" } } ================================================ FILE: examples/nestjs-example/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } ================================================ FILE: examples/nestjs-example/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": false, "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false } } ================================================ FILE: examples/nestjs-example/views/index.hbs ================================================ App

                Status:

                Transport:

                ================================================ FILE: examples/nextjs-app-router/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: examples/nextjs-app-router/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: examples/nextjs-app-router/jsconfig.json ================================================ { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ================================================ FILE: examples/nextjs-app-router/next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = {}; export default nextConfig; ================================================ FILE: examples/nextjs-app-router/package.json ================================================ { "name": "nextjs-app-router", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "next": "14.1.4", "react": "^18", "react-dom": "^18", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/nextjs-app-router/server.js ================================================ import { createServer } from "http"; import next from "next"; import { Server } from "socket.io"; const dev = process.env.NODE_ENV !== "production"; const hostname = "localhost"; const port = 3000; // when using middleware `hostname` and `port` must be provided below const app = next({ dev, hostname, port }); const handler = app.getRequestHandler(); app.prepare().then(() => { const httpServer = createServer(handler); const io = new Server(httpServer); io.on("connection", (socket) => { // ... }); httpServer .once("error", (err) => { console.error(err); process.exit(1); }) .listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`); }); }); ================================================ FILE: examples/nextjs-app-router/src/app/globals.css ================================================ :root { --max-width: 1100px; --border-radius: 12px; --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; --primary-glow: conic-gradient( from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg ); --secondary-glow: radial-gradient( rgba(255, 255, 255, 1), rgba(255, 255, 255, 0) ); --tile-start-rgb: 239, 245, 249; --tile-end-rgb: 228, 232, 233; --tile-border: conic-gradient( #00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080 ); --callout-rgb: 238, 240, 241; --callout-border-rgb: 172, 175, 176; --card-rgb: 180, 185, 188; --card-border-rgb: 131, 134, 135; } @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; --background-start-rgb: 0, 0, 0; --background-end-rgb: 0, 0, 0; --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); --secondary-glow: linear-gradient( to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3) ); --tile-start-rgb: 2, 13, 46; --tile-end-rgb: 2, 5, 19; --tile-border: conic-gradient( #ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80 ); --callout-rgb: 20, 20, 20; --callout-border-rgb: 108, 108, 108; --card-rgb: 100, 100, 100; --card-border-rgb: 200, 200, 200; } } * { box-sizing: border-box; padding: 0; margin: 0; } html, body { max-width: 100vw; overflow-x: hidden; } body { color: rgb(var(--foreground-rgb)); background: linear-gradient( to bottom, transparent, rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); } a { color: inherit; text-decoration: none; } @media (prefers-color-scheme: dark) { html { color-scheme: dark; } } ================================================ FILE: examples/nextjs-app-router/src/app/layout.js ================================================ import { Inter } from "next/font/google"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children }) { return ( {children} ); } ================================================ FILE: examples/nextjs-app-router/src/app/page.js ================================================ "use client"; import Image from "next/image"; import styles from "./page.module.css"; import { useEffect, useState } from "react"; import { socket } from "../socket"; export default function Home() { const [isConnected, setIsConnected] = useState(false); const [transport, setTransport] = useState("N/A"); useEffect(() => { if (socket.connected) { onConnect(); } function onConnect() { setIsConnected(true); setTransport(socket.io.engine.transport.name || "N/A"); socket.io.engine.on("upgrade", (transport) => { setTransport(transport.name); }); } function onDisconnect() { setIsConnected(false); setTransport("N/A") } socket.on("connect", onConnect); socket.on("disconnect", onDisconnect); return () => { socket.off("connect", onConnect); socket.off("disconnect", onDisconnect); }; }, []); return (

                Status: { isConnected ? "connected" : "disconnected" }

                Transport: { transport }

                ); } ================================================ FILE: examples/nextjs-app-router/src/app/page.module.css ================================================ .main { display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 6rem; min-height: 100vh; } .description { display: inherit; justify-content: inherit; align-items: inherit; font-size: 0.85rem; max-width: var(--max-width); width: 100%; z-index: 2; font-family: var(--font-mono); } .description a { display: flex; justify-content: center; align-items: center; gap: 0.5rem; } .description p { position: relative; margin: 0; padding: 1rem; background-color: rgba(var(--callout-rgb), 0.5); border: 1px solid rgba(var(--callout-border-rgb), 0.3); border-radius: var(--border-radius); } .code { font-weight: 700; font-family: var(--font-mono); } .grid { display: grid; grid-template-columns: repeat(4, minmax(25%, auto)); max-width: 100%; width: var(--max-width); } .card { padding: 1rem 1.2rem; border-radius: var(--border-radius); background: rgba(var(--card-rgb), 0); border: 1px solid rgba(var(--card-border-rgb), 0); transition: background 200ms, border 200ms; } .card span { display: inline-block; transition: transform 200ms; } .card h2 { font-weight: 600; margin-bottom: 0.7rem; } .card p { margin: 0; opacity: 0.6; font-size: 0.9rem; line-height: 1.5; max-width: 30ch; text-wrap: balance; } .center { display: flex; justify-content: center; align-items: center; position: relative; padding: 4rem 0; } .center::before { background: var(--secondary-glow); border-radius: 50%; width: 480px; height: 360px; margin-left: -400px; } .center::after { background: var(--primary-glow); width: 240px; height: 180px; z-index: -1; } .center::before, .center::after { content: ""; left: 50%; position: absolute; filter: blur(45px); transform: translateZ(0); } .logo { position: relative; } /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { .card:hover { background: rgba(var(--card-rgb), 0.1); border: 1px solid rgba(var(--card-border-rgb), 0.15); } .card:hover span { transform: translateX(4px); } } @media (prefers-reduced-motion) { .card:hover span { transform: none; } } /* Mobile */ @media (max-width: 700px) { .content { padding: 4rem; } .grid { grid-template-columns: 1fr; margin-bottom: 120px; max-width: 320px; text-align: center; } .card { padding: 1rem 2.5rem; } .card h2 { margin-bottom: 0.5rem; } .center { padding: 8rem 0 6rem; } .center::before { transform: none; height: 300px; } .description { font-size: 0.8rem; } .description a { padding: 1rem; } .description p, .description div { display: flex; justify-content: center; position: fixed; width: 100%; } .description p { align-items: center; inset: 0 0 auto; padding: 2rem 1rem 1.4rem; border-radius: 0; border: none; border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); background: linear-gradient( to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5) ); background-clip: padding-box; backdrop-filter: blur(24px); } .description div { align-items: flex-end; pointer-events: none; inset: auto 0 0; padding: 2rem; height: 200px; background: linear-gradient( to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40% ); z-index: 1; } } /* Tablet and Smaller Desktop */ @media (min-width: 701px) and (max-width: 1120px) { .grid { grid-template-columns: repeat(2, 50%); } } @media (prefers-color-scheme: dark) { .vercelLogo { filter: invert(1); } .logo { filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); } } @keyframes rotate { from { transform: rotate(360deg); } to { transform: rotate(0deg); } } ================================================ FILE: examples/nextjs-app-router/src/socket.js ================================================ "use client"; import { io } from "socket.io-client"; export const socket = io(); ================================================ FILE: examples/nextjs-pages-router/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ /src/.next/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: examples/nextjs-pages-router/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: examples/nextjs-pages-router/jsconfig.json ================================================ { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ================================================ FILE: examples/nextjs-pages-router/next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, }; export default nextConfig; ================================================ FILE: examples/nextjs-pages-router/package.json ================================================ { "name": "nextjs-pages-router", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "next lint" }, "dependencies": { "next": "14.1.4", "react": "^18", "react-dom": "^18", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/nextjs-pages-router/server.js ================================================ import { createServer } from "http"; import next from "next"; import { Server } from "socket.io"; const dev = process.env.NODE_ENV !== "production"; const hostname = "localhost"; const port = 3000; // when using middleware `hostname` and `port` must be provided below const app = next({ dev, hostname, port }); const handler = app.getRequestHandler(); app.prepare().then(() => { const httpServer = createServer(handler); const io = new Server(httpServer); io.on("connection", (socket) => { // ... }); httpServer .once("error", (err) => { console.error(err); process.exit(1); }) .listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`); }); }); ================================================ FILE: examples/nextjs-pages-router/src/pages/_app.js ================================================ // import "@/styles/globals.css"; export default function App({ Component, pageProps }) { return ; } ================================================ FILE: examples/nextjs-pages-router/src/pages/_document.js ================================================ import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return (
                ); } ================================================ FILE: examples/nextjs-pages-router/src/pages/api/hello.js ================================================ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction export default function handler(req, res) { res.status(200).json({ name: "John Doe" }); } ================================================ FILE: examples/nextjs-pages-router/src/pages/index.js ================================================ import Head from "next/head"; import Image from "next/image"; import { Inter } from "next/font/google"; // import styles from "@/styles/Home.module.css"; import { useEffect, useState } from "react"; import { socket } from "../socket"; const inter = Inter({ subsets: ["latin"] }); export default function Home() { const [isConnected, setIsConnected] = useState(false); const [transport, setTransport] = useState("N/A"); useEffect(() => { if (socket.connected) { onConnect(); } function onConnect() { setIsConnected(true); setTransport(socket.io.engine.transport.name); socket.io.engine.on("upgrade", (transport) => { setTransport(transport.name); }); } function onDisconnect() { setIsConnected(false); setTransport("N/A") } socket.on("connect", onConnect); socket.on("disconnect", onDisconnect); return () => { socket.off("connect", onConnect); socket.off("disconnect", onDisconnect); }; }, []); return ( <> Create Next App

                Status: { isConnected ? "connected" : "disconnected" }

                Transport: { transport }

                ); } ================================================ FILE: examples/nextjs-pages-router/src/socket.js ================================================ import { io } from "socket.io-client"; const isBrowser = typeof window !== "undefined"; // only create the Socket.IO client on the client side (no ssr) export const socket = isBrowser ? io() : {}; ================================================ FILE: examples/nextjs-pages-router/src/styles/Home.module.css ================================================ .main { display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 6rem; min-height: 100vh; } .description { display: inherit; justify-content: inherit; align-items: inherit; font-size: 0.85rem; max-width: var(--max-width); width: 100%; z-index: 2; font-family: var(--font-mono); } .description a { display: flex; justify-content: center; align-items: center; gap: 0.5rem; } .description p { position: relative; margin: 0; padding: 1rem; background-color: rgba(var(--callout-rgb), 0.5); border: 1px solid rgba(var(--callout-border-rgb), 0.3); border-radius: var(--border-radius); } .code { font-weight: 700; font-family: var(--font-mono); } .grid { display: grid; grid-template-columns: repeat(4, minmax(25%, auto)); max-width: var(--max-width); width: 100%; } .card { padding: 1rem 1.2rem; border-radius: var(--border-radius); background: rgba(var(--card-rgb), 0); border: 1px solid rgba(var(--card-border-rgb), 0); transition: background 200ms, border 200ms; } .card span { display: inline-block; transition: transform 200ms; } .card h2 { font-weight: 600; margin-bottom: 0.7rem; } .card p { margin: 0; opacity: 0.6; font-size: 0.9rem; line-height: 1.5; max-width: 30ch; } .center { display: flex; justify-content: center; align-items: center; position: relative; padding: 4rem 0; } .center::before { background: var(--secondary-glow); border-radius: 50%; width: 480px; height: 360px; margin-left: -400px; } .center::after { background: var(--primary-glow); width: 240px; height: 180px; z-index: -1; } .center::before, .center::after { content: ""; left: 50%; position: absolute; filter: blur(45px); transform: translateZ(0); } .logo { position: relative; } /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { .card:hover { background: rgba(var(--card-rgb), 0.1); border: 1px solid rgba(var(--card-border-rgb), 0.15); } .card:hover span { transform: translateX(4px); } } @media (prefers-reduced-motion) { .card:hover span { transform: none; } } /* Mobile */ @media (max-width: 700px) { .content { padding: 4rem; } .grid { grid-template-columns: 1fr; margin-bottom: 120px; max-width: 320px; text-align: center; } .card { padding: 1rem 2.5rem; } .card h2 { margin-bottom: 0.5rem; } .center { padding: 8rem 0 6rem; } .center::before { transform: none; height: 300px; } .description { font-size: 0.8rem; } .description a { padding: 1rem; } .description p, .description div { display: flex; justify-content: center; position: fixed; width: 100%; } .description p { align-items: center; inset: 0 0 auto; padding: 2rem 1rem 1.4rem; border-radius: 0; border: none; border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); background: linear-gradient( to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5) ); background-clip: padding-box; backdrop-filter: blur(24px); } .description div { align-items: flex-end; pointer-events: none; inset: auto 0 0; padding: 2rem; height: 200px; background: linear-gradient( to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40% ); z-index: 1; } } /* Tablet and Smaller Desktop */ @media (min-width: 701px) and (max-width: 1120px) { .grid { grid-template-columns: repeat(2, 50%); } } @media (prefers-color-scheme: dark) { .vercelLogo { filter: invert(1); } .logo { filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); } } @keyframes rotate { from { transform: rotate(360deg); } to { transform: rotate(0deg); } } ================================================ FILE: examples/nextjs-pages-router/src/styles/globals.css ================================================ :root { --max-width: 1100px; --border-radius: 12px; --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; --primary-glow: conic-gradient( from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg ); --secondary-glow: radial-gradient( rgba(255, 255, 255, 1), rgba(255, 255, 255, 0) ); --tile-start-rgb: 239, 245, 249; --tile-end-rgb: 228, 232, 233; --tile-border: conic-gradient( #00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080 ); --callout-rgb: 238, 240, 241; --callout-border-rgb: 172, 175, 176; --card-rgb: 180, 185, 188; --card-border-rgb: 131, 134, 135; } @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; --background-start-rgb: 0, 0, 0; --background-end-rgb: 0, 0, 0; --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); --secondary-glow: linear-gradient( to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3) ); --tile-start-rgb: 2, 13, 46; --tile-end-rgb: 2, 5, 19; --tile-border: conic-gradient( #ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80 ); --callout-rgb: 20, 20, 20; --callout-border-rgb: 108, 108, 108; --card-rgb: 100, 100, 100; --card-border-rgb: 200, 200, 200; } } * { box-sizing: border-box; padding: 0; margin: 0; } html, body { max-width: 100vw; overflow-x: hidden; } body { color: rgb(var(--foreground-rgb)); background: linear-gradient( to bottom, transparent, rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); } a { color: inherit; text-decoration: none; } @media (prefers-color-scheme: dark) { html { color-scheme: dark; } } ================================================ FILE: examples/nuxt-example/.gitignore ================================================ # Nuxt dev/build outputs .output .data .nuxt .nitro .cache dist # Node dependencies node_modules # Logs logs *.log # Misc .DS_Store .fleet .idea # Local env files .env .env.* !.env.example ================================================ FILE: examples/nuxt-example/README.md ================================================ # Nuxt 3 Minimal Starter Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. ## Setup Make sure to install the dependencies: ```bash # npm npm install # pnpm pnpm install # yarn yarn install # bun bun install ``` ## Development Server Start the development server on `http://localhost:3000`: ```bash # npm npm run dev # pnpm pnpm run dev # yarn yarn dev # bun bun run dev ``` ## Production Build the application for production: ```bash # npm npm run build # pnpm pnpm run build # yarn yarn build # bun bun run build ``` Locally preview production build: ```bash # npm npm run preview # pnpm pnpm run preview # yarn yarn preview # bun bun run preview ``` Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. ================================================ FILE: examples/nuxt-example/app.vue ================================================ ================================================ FILE: examples/nuxt-example/components/Connection.client.vue ================================================ ================================================ FILE: examples/nuxt-example/components/socket.ts ================================================ import { io } from "socket.io-client"; export const socket = io(); ================================================ FILE: examples/nuxt-example/nuxt.config.ts ================================================ // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ devtools: { enabled: true }, nitro: { experimental: { websocket: true }, } }) ================================================ FILE: examples/nuxt-example/package.json ================================================ { "name": "nuxt-app", "private": true, "type": "module", "scripts": { "build": "nuxt build", "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", "postinstall": "nuxt prepare" }, "dependencies": { "nuxt": "^3.11.1", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", "vue": "^3.4.21", "vue-router": "^4.3.0" } } ================================================ FILE: examples/nuxt-example/server/plugins/socket.io.ts ================================================ import type { NitroApp } from "nitropack"; import { Server as Engine } from "engine.io"; import { Server } from "socket.io"; import { defineEventHandler } from "h3"; export default defineNitroPlugin((nitroApp: NitroApp) => { const engine = new Engine(); const io = new Server(); io.bind(engine); io.on("connection", (socket) => { // ... }); nitroApp.router.use("/socket.io/", defineEventHandler({ handler(event) { engine.handleRequest(event.node.req, event.node.res); event._handled = true; }, websocket: { open(peer) { // crossws >= 0.3.0 // @ts-expect-error private method and property engine.prepare(peer._internal.nodeReq); // @ts-expect-error private method and property engine.onWebSocket(peer._internal.nodeReq, peer._internal.nodeReq.socket, peer.websocket); // crossws < 0.3.0 // const context = peer.ctx.node; // // @ts-expect-error private method // engine.prepare(context.req); // // @ts-expect-error private method // engine.onWebSocket(context.req, context.req.socket, context.ws); } } })); }); ================================================ FILE: examples/nuxt-example/server/tsconfig.json ================================================ { "extends": "../.nuxt/tsconfig.server.json" } ================================================ FILE: examples/nuxt-example/tsconfig.json ================================================ { // https://nuxt.com/docs/guide/concepts/typescript "extends": "./.nuxt/tsconfig.json" } ================================================ FILE: examples/nwjs-example/README.md ================================================ # Socket.IO with [NW.js](https://nwjs.io/) Guide: https://socket.io/how-to/use-with-nwjs ## How to use ### Client ```bash # install the dependencies $ npm i # start the app $ nw . ``` ### Server ```bash $ cd server # install the dependencies $ npm i # start the server $ npm start ``` ================================================ FILE: examples/nwjs-example/index.html ================================================

                Status:

                Transport:

                ================================================ FILE: examples/nwjs-example/index.js ================================================ const { io } = require("socket.io-client"); const socket = io("http://localhost:3000"); exports.registerListeners = function ({ statusSpan, transportSpan }) { function onConnect() { statusSpan.innerText = "Connected"; transportSpan.innerText = socket.io.engine.transport.name; socket.io.engine.on("upgrade", (transport) => { transportSpan.innerText = transport.name; }); console.log(`connect ${socket.id}`); } if (socket.connected) { onConnect(); } socket.on("connect", onConnect); socket.on("connect_error", (err) => { console.log(`connect_error due to ${err.message}`); }); socket.on("disconnect", (reason) => { statusSpan.innerText = "Disconnected"; transportSpan.innerText = "N/A"; console.log(`disconnect due to ${reason}`); }); } exports.emit = function (...args) { socket.emit(...args); } ================================================ FILE: examples/nwjs-example/package.json ================================================ { "name": "helloworld", "main": "index.html", "dependencies": { "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/nwjs-example/server/index.js ================================================ import { Server } from 'socket.io'; const io = new Server(); io.on('connection', (socket) => { console.log(`connect: ${socket.id}`); socket.on("hello", (val) => { console.log(val); }); socket.on('disconnect', () => { console.log(`disconnect: ${socket.id}`); }); }); io.listen(3000); ================================================ FILE: examples/nwjs-example/server/package.json ================================================ { "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "start": "node index.js" }, "author": "", "license": "ISC", "dependencies": { "socket.io": "^4.7.5" } } ================================================ FILE: examples/passport-example/README.md ================================================ # Example with [Passport](http://www.passportjs.org/) This example shows how to retrieve the authentication context from a basic [Express](http://expressjs.com/) + [Passport](http://www.passportjs.org/) application. ![Passport example](assets/passport_example.gif) Please read the related guide: https://socket.io/how-to/use-with-passport ## How to use ``` $ npm ci && npm start ``` And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable. ## How it works The Socket.IO server retrieves the user context from the session: ```js function onlyForHandshake(middleware) { return (req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { middleware(req, res, next); } else { next(); } }; } io.engine.use(onlyForHandshake(sessionMiddleware)); io.engine.use(onlyForHandshake(passport.session())); io.engine.use( onlyForHandshake((req, res, next) => { if (req.user) { next(); } else { res.writeHead(401); res.end(); } }), ); ``` ================================================ FILE: examples/passport-example/cjs/index.html ================================================ Passport example

                Authenticated!

                Status Disconnected
                Socket ID
                Username
                ================================================ FILE: examples/passport-example/cjs/index.js ================================================ const express = require("express"); const { createServer } = require("node:http"); const { Server } = require("socket.io"); const session = require("express-session"); const bodyParser = require("body-parser"); const passport = require("passport"); const LocalStrategy = require("passport-local").Strategy; const { join } = require("node:path"); const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.use(bodyParser.urlencoded({ extended: false })); app.use(passport.session()); app.get("/", (req, res) => { if (!req.user) { return res.redirect("/login"); } res.sendFile(join(__dirname, "index.html")); }); app.get("/login", (req, res) => { if (req.user) { return res.redirect("/"); } res.sendFile(join(__dirname, "login.html")); }); app.post( "/login", passport.authenticate("local", { successRedirect: "/", failureRedirect: "/", }), ); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(`session:${sessionId}`).disconnectSockets(); res.status(204).end(); }); }); passport.use( new LocalStrategy((username, password, done) => { if (username === "john" && password === "changeit") { console.log("authentication OK"); return done(null, { id: 1, username }); } else { console.log("wrong credentials"); return done(null, false); } }), ); passport.serializeUser((user, cb) => { console.log(`serializeUser ${user.id}`); cb(null, user); }); passport.deserializeUser((user, cb) => { console.log(`deserializeUser ${user.id}`); cb(null, user); }); const io = new Server(httpServer); function onlyForHandshake(middleware) { return (req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { middleware(req, res, next); } else { next(); } }; } io.engine.use(onlyForHandshake(sessionMiddleware)); io.engine.use(onlyForHandshake(passport.session())); io.engine.use( onlyForHandshake((req, res, next) => { if (req.user) { next(); } else { res.writeHead(401); res.end(); } }), ); io.on("connection", (socket) => { const req = socket.request; socket.join(`session:${req.session.id}`); socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-example/cjs/login.html ================================================ Passport example

                Not authenticated


                ================================================ FILE: examples/passport-example/cjs/package.json ================================================ { "name": "passport-example", "version": "0.0.1", "private": true, "type": "commonjs", "description": "Example with passport (https://www.passportjs.org)", "scripts": { "start": "node index.js" }, "dependencies": { "express": "~4.17.3", "express-session": "~1.17.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "socket.io": "^4.7.2" }, "devDependencies": { "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-example/esm/index.html ================================================ Passport example

                Authenticated!

                Status Disconnected
                Socket ID
                Username
                ================================================ FILE: examples/passport-example/esm/index.js ================================================ import express from "express"; import { createServer } from "http"; import { Server } from "socket.io"; import session from "express-session"; import bodyParser from "body-parser"; import passport from "passport"; import { Strategy as LocalStrategy } from "passport-local"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.use(bodyParser.urlencoded({ extended: false })); app.use(passport.session()); const __dirname = dirname(fileURLToPath(import.meta.url)); app.get("/", (req, res) => { if (!req.user) { return res.redirect("/login"); } res.sendFile(join(__dirname, "index.html")); }); app.get("/login", (req, res) => { if (req.user) { return res.redirect("/"); } res.sendFile(join(__dirname, "login.html")); }); app.post( "/login", passport.authenticate("local", { successRedirect: "/", failureRedirect: "/", }), ); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(`session:${sessionId}`).disconnectSockets(); res.status(204).end(); }); }); passport.use( new LocalStrategy((username, password, done) => { if (username === "john" && password === "changeit") { console.log("authentication OK"); return done(null, { id: 1, username }); } else { console.log("wrong credentials"); return done(null, false); } }), ); passport.serializeUser((user, cb) => { console.log(`serializeUser ${user.id}`); cb(null, user); }); passport.deserializeUser((user, cb) => { console.log(`deserializeUser ${user.id}`); cb(null, user); }); const io = new Server(httpServer); function onlyForHandshake(middleware) { return (req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { middleware(req, res, next); } else { next(); } }; } io.engine.use(onlyForHandshake(sessionMiddleware)); io.engine.use(onlyForHandshake(passport.session())); io.engine.use( onlyForHandshake((req, res, next) => { if (req.user) { next(); } else { res.writeHead(401); res.end(); } }), ); io.on("connection", (socket) => { const req = socket.request; socket.join(`session:${req.session.id}`); socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-example/esm/login.html ================================================ Passport example

                Not authenticated


                ================================================ FILE: examples/passport-example/esm/package.json ================================================ { "name": "passport-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with passport (https://www.passportjs.org)", "scripts": { "start": "node index.js" }, "dependencies": { "express": "~4.17.3", "express-session": "~1.17.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "socket.io": "^4.7.2" }, "devDependencies": { "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-example/ts/index.html ================================================ Passport example

                Authenticated!

                Status Disconnected
                Socket ID
                Username
                ================================================ FILE: examples/passport-example/ts/index.ts ================================================ import express = require("express"); import { createServer } from "http"; import { Server } from "socket.io"; import session from "express-session"; import { type Request, type Response } from "express"; import bodyParser = require("body-parser"); import passport = require("passport"); import { Strategy as LocalStrategy } from "passport-local"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; declare global { namespace Express { interface User { id: number; username: string; } } } const port = process.env.PORT || 3000; const app = express(); const httpServer = createServer(app); const sessionMiddleware = session({ secret: "changeit", resave: true, saveUninitialized: true, }); app.use(sessionMiddleware); app.use(bodyParser.urlencoded({ extended: false })); app.use(passport.initialize()); app.use(passport.session()); const __dirname = dirname(fileURLToPath(import.meta.url)); app.get("/", (req, res) => { if (!req.user) { return res.redirect("/login"); } res.sendFile(join(__dirname, "index.html")); }); app.get("/login", (req, res) => { if (req.user) { return res.redirect("/"); } res.sendFile(join(__dirname, "login.html")); }); app.post( "/login", passport.authenticate("local", { successRedirect: "/", failureRedirect: "/", }), ); app.post("/logout", (req, res) => { const sessionId = req.session.id; req.session.destroy(() => { // disconnect all Socket.IO connections linked to this session ID io.to(`session:${sessionId}`).disconnectSockets(); res.status(204).end(); }); }); passport.use( new LocalStrategy((username, password, done) => { if (username === "john" && password === "changeit") { console.log("authentication OK"); return done(null, { id: 1, username }); } else { console.log("wrong credentials"); return done(null, false); } }), ); passport.serializeUser((user, cb) => { console.log(`serializeUser ${user.id}`); cb(null, user); }); passport.deserializeUser((user: Express.User, cb) => { console.log(`deserializeUser ${user.id}`); cb(null, user); }); const io = new Server(httpServer); function onlyForHandshake( middleware: (req: Request, res: Response, next: any) => void, ) { return ( req: Request & { _query: Record }, res: Response, next: (err?: Error) => void, ) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { middleware(req, res, next); } else { next(); } }; } io.engine.use(onlyForHandshake(sessionMiddleware)); io.engine.use(onlyForHandshake(passport.session())); io.engine.use( onlyForHandshake((req, res, next) => { if (req.user) { next(); } else { res.writeHead(401); res.end(); } }), ); io.on("connection", (socket) => { const req = socket.request as Request & { user: Express.User }; socket.join(`session:${req.session.id}`); socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-example/ts/login.html ================================================ Passport example

                Not authenticated


                ================================================ FILE: examples/passport-example/ts/package.json ================================================ { "name": "passport-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with passport (https://www.passportjs.org)", "scripts": { "start": "ts-node index.ts" }, "dependencies": { "@types/express": "^4.17.17", "@types/express-session": "^1.17.7", "@types/node": "^20.6.0", "@types/passport": "^1.0.16", "express": "~4.17.3", "express-session": "~1.17.2", "passport": "^0.7.0", "passport-local": "^1.0.0", "socket.io": "^4.7.2", "ts-node": "^10.9.1", "typescript": "^5.2.2" }, "devDependencies": { "@types/passport-local": "^1.0.38", "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-example/ts/tsconfig.json ================================================ { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "strict": true }, "ts-node": { "esm": true } } ================================================ FILE: examples/passport-jwt-example/README.md ================================================ # Example with [`passport-jwt`](https://www.passportjs.org/packages/passport-jwt/) This example shows how to retrieve the authentication context from a basic [Express](http://expressjs.com/) + [Passport](http://www.passportjs.org/) application. ![Passport example](assets/passport_example.gif) Please read the related guide: https://socket.io/how-to/use-with-jwt ## How to use ``` $ npm ci && npm start ``` And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable. ## How it works The client sends the JWT in the headers: ```js const socket = io({ extraHeaders: { authorization: `bearer token` } }); ``` And the Socket.IO server then parses the token and retrieves the user context: ```js io.engine.use((req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { passport.authenticate("jwt", { session: false })(req, res, next); } else { next(); } }); ``` ================================================ FILE: examples/passport-jwt-example/cjs/index.html ================================================ Passport JWT example ================================================ FILE: examples/passport-jwt-example/cjs/index.js ================================================ const express = require("express"); const { createServer } = require("node:http"); const { join } = require("node:path"); const passport = require("passport"); const passportJwt = require("passport-jwt"); const JwtStrategy = passportJwt.Strategy; const ExtractJwt = passportJwt.ExtractJwt; const bodyParser = require("body-parser"); const { Server } = require("socket.io"); const jwt = require("jsonwebtoken"); const port = process.env.PORT || 3000; const jwtSecret = "Mys3cr3t"; const app = express(); const httpServer = createServer(app); app.use(bodyParser.json()); app.get("/", (req, res) => { res.sendFile(join(__dirname, "index.html")); }); app.get( "/self", passport.authenticate("jwt", { session: false }), (req, res) => { if (req.user) { res.send(req.user); } else { res.status(401).end(); } }, ); app.post("/login", (req, res) => { if (req.body.username === "john" && req.body.password === "changeit") { console.log("authentication OK"); const user = { id: 1, username: "john", }; const token = jwt.sign( { data: user, }, jwtSecret, { issuer: "accounts.examplesoft.com", audience: "yoursite.net", expiresIn: "1h", }, ); res.json({ token }); } else { console.log("wrong credentials"); res.status(401).end(); } }); const jwtDecodeOptions = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: jwtSecret, issuer: "accounts.examplesoft.com", audience: "yoursite.net", }; passport.use( new JwtStrategy(jwtDecodeOptions, (payload, done) => { return done(null, payload.data); }), ); const io = new Server(httpServer); io.engine.use((req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { passport.authenticate("jwt", { session: false })(req, res, next); } else { next(); } }); io.on("connection", (socket) => { const req = socket.request; socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-jwt-example/cjs/package.json ================================================ { "name": "passport-jwt-example", "version": "0.0.1", "private": true, "type": "commonjs", "description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)", "scripts": { "start": "node index.js" }, "dependencies": { "body-parser": "^1.20.2", "express": "~4.17.3", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "socket.io": "^4.7.2" }, "devDependencies": { "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-jwt-example/esm/index.html ================================================ Passport JWT example ================================================ FILE: examples/passport-jwt-example/esm/index.js ================================================ import express from "express"; import { createServer } from "node:http"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import passport from "passport"; import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; import bodyParser from "body-parser"; import { Server } from "socket.io"; import jwt from "jsonwebtoken"; const port = process.env.PORT || 3000; const jwtSecret = "Mys3cr3t"; const app = express(); const httpServer = createServer(app); app.use(bodyParser.json()); const __dirname = dirname(fileURLToPath(import.meta.url)); app.get("/", (req, res) => { res.sendFile(join(__dirname, "index.html")); }); app.get( "/self", passport.authenticate("jwt", { session: false }), (req, res) => { if (req.user) { res.send(req.user); } else { res.status(401).end(); } }, ); app.post("/login", (req, res) => { if (req.body.username === "john" && req.body.password === "changeit") { console.log("authentication OK"); const user = { id: 1, username: "john", }; const token = jwt.sign( { data: user, }, jwtSecret, { issuer: "accounts.examplesoft.com", audience: "yoursite.net", expiresIn: "1h", }, ); res.json({ token }); } else { console.log("wrong credentials"); res.status(401).end(); } }); const jwtDecodeOptions = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: jwtSecret, issuer: "accounts.examplesoft.com", audience: "yoursite.net", }; passport.use( new JwtStrategy(jwtDecodeOptions, (payload, done) => { return done(null, payload.data); }), ); const io = new Server(httpServer); io.engine.use((req, res, next) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { passport.authenticate("jwt", { session: false })(req, res, next); } else { next(); } }); io.on("connection", (socket) => { const req = socket.request; socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-jwt-example/esm/package.json ================================================ { "name": "passport-jwt-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)", "scripts": { "start": "node index.js" }, "dependencies": { "body-parser": "^1.20.2", "express": "~4.17.3", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "socket.io": "^4.7.2" }, "devDependencies": { "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-jwt-example/ts/index.html ================================================ Passport JWT example ================================================ FILE: examples/passport-jwt-example/ts/index.ts ================================================ import express from "express"; import { type Request, type Response } from "express"; import { createServer } from "node:http"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import passport from "passport"; import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; import bodyParser from "body-parser"; import { Server } from "socket.io"; import jwt from "jsonwebtoken"; declare global { namespace Express { interface User { id: number; username: string; } } } const port = process.env.PORT || 3000; const jwtSecret = "Mys3cr3t"; const app = express(); const httpServer = createServer(app); app.use(bodyParser.json()); const __dirname = dirname(fileURLToPath(import.meta.url)); app.get("/", (req, res) => { res.sendFile(join(__dirname, "index.html")); }); app.get( "/self", passport.authenticate("jwt", { session: false }), (req, res) => { if (req.user) { res.send(req.user); } else { res.status(401).end(); } }, ); app.post("/login", (req, res) => { if (req.body.username === "john" && req.body.password === "changeit") { console.log("authentication OK"); const user = { id: 1, username: "john", }; const token = jwt.sign( { data: user, }, jwtSecret, { issuer: "accounts.examplesoft.com", audience: "yoursite.net", expiresIn: "1h", }, ); res.json({ token }); } else { console.log("wrong credentials"); res.status(401).end(); } }); const jwtDecodeOptions = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: jwtSecret, issuer: "accounts.examplesoft.com", audience: "yoursite.net", }; passport.use( new JwtStrategy(jwtDecodeOptions, (payload, done) => { return done(null, payload.data); }), ); const io = new Server(httpServer); io.engine.use( (req: { _query: Record }, res: Response, next: Function) => { const isHandshake = req._query.sid === undefined; if (isHandshake) { passport.authenticate("jwt", { session: false })(req, res, next); } else { next(); } }, ); io.on("connection", (socket) => { const req = socket.request as Request & { user: Express.User }; socket.join(`user:${req.user.id}`); socket.on("whoami", (cb) => { cb(req.user.username); }); }); httpServer.listen(port, () => { console.log(`application is running at: http://localhost:${port}`); }); ================================================ FILE: examples/passport-jwt-example/ts/package.json ================================================ { "name": "passport-jwt-example", "version": "0.0.1", "private": true, "type": "module", "description": "Example with passport and JWT (https://www.passportjs.org/packages/passport-jwt/)", "scripts": { "start": "node index.js" }, "dependencies": { "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.5", "@types/passport": "^1.0.16", "@types/passport-jwt": "^4.0.0", "body-parser": "^1.20.2", "express": "~4.17.3", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "socket.io": "^4.7.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, "devDependencies": { "prettier": "^3.1.1" } } ================================================ FILE: examples/passport-jwt-example/ts/tsconfig.json ================================================ { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "strict": true }, "ts-node": { "esm": true } } ================================================ FILE: examples/postgres-adapter-example/README.md ================================================ # Example with `@socket.io/postgres-adapter` **Table of contents** * [How to use](#how-to-use) * [Documentation](#documentation) ## How to use ```bash # start the postgres server $ docker compose up -d # run the cluster $ node cluster.js # run the client $ node client.js ``` ## Documentation The documentation can be found here: https://socket.io/docs/v4/postgres-adapter/ ================================================ FILE: examples/postgres-adapter-example/client.js ================================================ import { io } from "socket.io-client"; const CLIENTS_COUNT = 3; const PORTS = [3000, 3001, 3002]; for (let i = 0; i < CLIENTS_COUNT; i++) { const socket = io(`ws://localhost:${PORTS[i % 3]}`, { // transports: ["polling"], // transports: ["websocket"], }); socket.on("connect", () => { console.log(`connected as ${socket.id}`); }); socket.on("connect_error", () => { console.log(`connect_error`); }); socket.on("disconnect", (reason) => { console.log(`disconnected due to ${reason}`); }); socket.on("hello", (socketId, pid) => { console.log(`received "hello" from ${socketId} (process: ${pid})`); }); setInterval(() => { socket.emit("hello"); }, 2000); } ================================================ FILE: examples/postgres-adapter-example/cluster.js ================================================ import cluster from 'node:cluster'; const SERVERS_COUNT = 3; cluster.setupPrimary({ exec: 'server.js', }); for (let i = 0; i < SERVERS_COUNT; i++) { cluster.fork({ PORT: 3000 + i }); } ================================================ FILE: examples/postgres-adapter-example/compose.yaml ================================================ services: postgres: image: postgres:14 ports: - "5432:5432" environment: POSTGRES_PASSWORD: "changeit" ================================================ FILE: examples/postgres-adapter-example/package.json ================================================ { "private": true, "name": "postgres-adapter-example", "version": "0.0.1", "type": "module", "dependencies": { "@socket.io/postgres-adapter": "^0.4.0", "pg": "^8.12.0", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5" } } ================================================ FILE: examples/postgres-adapter-example/server.js ================================================ import { Server } from "socket.io"; import { createAdapter } from "@socket.io/postgres-adapter"; import pg from "pg"; import process from "node:process"; const PORT = process.env.PORT || 3000; const pool = new pg.Pool({ user: "postgres", host: "localhost", database: "postgres", password: "changeit", port: 5432, }); await pool.query(` CREATE TABLE IF NOT EXISTS socket_io_attachments ( id bigserial UNIQUE, created_at timestamptz DEFAULT NOW(), payload bytea ); `); pool.on("error", (err) => { console.error("Postgres error", err); }); const io = new Server({ adapter: createAdapter(pool) }); io.on("connection", (socket) => { socket.on("hello", () => { // send to anyone except the sender socket.broadcast.emit("hello", socket.id, process.pid); }); }); io.listen(PORT); console.log(`server listening on port ${PORT}`); ================================================ FILE: examples/private-messaging/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? package-lock.json ================================================ FILE: examples/private-messaging/README.md ================================================ # Private messaging with Socket.IO Please read the related guide: - [Part I](https://socket.io/get-started/private-messaging-part-1/): initial implementation - [Part II](https://socket.io/get-started/private-messaging-part-2/): persistent user ID - [Part III](https://socket.io/get-started/private-messaging-part-3/): persistent messages - [Part IV](https://socket.io/get-started/private-messaging-part-4/): scaling up ## Running the frontend ``` npm install npm run serve ``` ### Running the server ``` cd server npm install npm start ``` ================================================ FILE: examples/private-messaging/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: examples/private-messaging/package.json ================================================ { "name": "private-messaging", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.8.3", "socket.io-client": "^4.0.0", "vue": "^2.6.14" }, "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-service": "~5.0.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^8.0.3", "vue-template-compiler": "^2.6.14" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "@babel/eslint-parser" }, "rules": { "vue/multi-word-component-names": "off" } }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } ================================================ FILE: examples/private-messaging/public/index.html ================================================ Private messaging with Socket.IO
                ================================================ FILE: examples/private-messaging/server/cluster.js ================================================ const cluster = require("cluster"); const http = require("http"); const { setupMaster } = require("@socket.io/sticky"); const WORKERS_COUNT = 4; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); for (let i = 0; i < WORKERS_COUNT; i++) { cluster.fork(); } cluster.on("exit", (worker) => { console.log(`Worker ${worker.process.pid} died`); cluster.fork(); }); const httpServer = http.createServer(); setupMaster(httpServer, { loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection" }); const PORT = process.env.PORT || 3000; httpServer.listen(PORT, () => console.log(`server listening at http://localhost:${PORT}`) ); } else { console.log(`Worker ${process.pid} started`); require("./index"); } ================================================ FILE: examples/private-messaging/server/docker-compose.yml ================================================ version: "3" services: redis: image: redis:5 ports: - "6379:6379" ================================================ FILE: examples/private-messaging/server/index.js ================================================ const httpServer = require("http").createServer(); const Redis = require("ioredis"); const redisClient = new Redis(); const io = require("socket.io")(httpServer, { cors: { origin: "http://localhost:8080", }, adapter: require("socket.io-redis")({ pubClient: redisClient, subClient: redisClient.duplicate(), }), }); const { setupWorker } = require("@socket.io/sticky"); const crypto = require("crypto"); const randomId = () => crypto.randomBytes(8).toString("hex"); const { RedisSessionStore } = require("./sessionStore"); const sessionStore = new RedisSessionStore(redisClient); const { RedisMessageStore } = require("./messageStore"); const messageStore = new RedisMessageStore(redisClient); io.use(async (socket, next) => { const sessionID = socket.handshake.auth.sessionID; if (sessionID) { const session = await sessionStore.findSession(sessionID); if (session) { socket.sessionID = sessionID; socket.userID = session.userID; socket.username = session.username; return next(); } } const username = socket.handshake.auth.username; if (!username) { return next(new Error("invalid username")); } socket.sessionID = randomId(); socket.userID = randomId(); socket.username = username; next(); }); io.on("connection", async (socket) => { // persist session sessionStore.saveSession(socket.sessionID, { userID: socket.userID, username: socket.username, connected: true, }); // emit session details socket.emit("session", { sessionID: socket.sessionID, userID: socket.userID, }); // join the "userID" room socket.join(socket.userID); // fetch existing users const users = []; const [messages, sessions] = await Promise.all([ messageStore.findMessagesForUser(socket.userID), sessionStore.findAllSessions(), ]); const messagesPerUser = new Map(); messages.forEach((message) => { const { from, to } = message; const otherUser = socket.userID === from ? to : from; if (messagesPerUser.has(otherUser)) { messagesPerUser.get(otherUser).push(message); } else { messagesPerUser.set(otherUser, [message]); } }); sessions.forEach((session) => { users.push({ userID: session.userID, username: session.username, connected: session.connected, messages: messagesPerUser.get(session.userID) || [], }); }); socket.emit("users", users); // notify existing users socket.broadcast.emit("user connected", { userID: socket.userID, username: socket.username, connected: true, messages: [], }); // forward the private message to the right recipient (and to other tabs of the sender) socket.on("private message", ({ content, to }) => { const message = { content, from: socket.userID, to, }; socket.to(to).to(socket.userID).emit("private message", message); messageStore.saveMessage(message); }); // notify users upon disconnection socket.on("disconnect", async () => { const matchingSockets = await io.in(socket.userID).allSockets(); const isDisconnected = matchingSockets.size === 0; if (isDisconnected) { // notify other users socket.broadcast.emit("user disconnected", socket.userID); // update the connection status of the session sessionStore.saveSession(socket.sessionID, { userID: socket.userID, username: socket.username, connected: false, }); } }); }); setupWorker(io); ================================================ FILE: examples/private-messaging/server/messageStore.js ================================================ /* abstract */ class MessageStore { saveMessage(message) {} findMessagesForUser(userID) {} } class InMemoryMessageStore extends MessageStore { constructor() { super(); this.messages = []; } saveMessage(message) { this.messages.push(message); } findMessagesForUser(userID) { return this.messages.filter( ({ from, to }) => from === userID || to === userID ); } } const CONVERSATION_TTL = 24 * 60 * 60; class RedisMessageStore extends MessageStore { constructor(redisClient) { super(); this.redisClient = redisClient; } saveMessage(message) { const value = JSON.stringify(message); this.redisClient .multi() .rpush(`messages:${message.from}`, value) .rpush(`messages:${message.to}`, value) .expire(`messages:${message.from}`, CONVERSATION_TTL) .expire(`messages:${message.to}`, CONVERSATION_TTL) .exec(); } findMessagesForUser(userID) { return this.redisClient .lrange(`messages:${userID}`, 0, -1) .then((results) => { return results.map((result) => JSON.parse(result)); }); } } module.exports = { InMemoryMessageStore, RedisMessageStore, }; ================================================ FILE: examples/private-messaging/server/package.json ================================================ { "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node cluster.js" }, "author": "Damien Arrachequesne ", "license": "MIT", "dependencies": { "@socket.io/sticky": "^1.0.0", "ioredis": "^4.22.0", "socket.io": "^4.0.0", "socket.io-redis": "^6.0.1" } } ================================================ FILE: examples/private-messaging/server/sessionStore.js ================================================ /* abstract */ class SessionStore { findSession(id) {} saveSession(id, session) {} findAllSessions() {} } class InMemorySessionStore extends SessionStore { constructor() { super(); this.sessions = new Map(); } findSession(id) { return this.sessions.get(id); } saveSession(id, session) { this.sessions.set(id, session); } findAllSessions() { return [...this.sessions.values()]; } } const SESSION_TTL = 24 * 60 * 60; const mapSession = ([userID, username, connected]) => userID ? { userID, username, connected: connected === "true" } : undefined; class RedisSessionStore extends SessionStore { constructor(redisClient) { super(); this.redisClient = redisClient; } findSession(id) { return this.redisClient .hmget(`session:${id}`, "userID", "username", "connected") .then(mapSession); } saveSession(id, { userID, username, connected }) { this.redisClient .multi() .hset( `session:${id}`, "userID", userID, "username", username, "connected", connected ) .expire(`session:${id}`, SESSION_TTL) .exec(); } async findAllSessions() { const keys = new Set(); let nextIndex = 0; do { const [nextIndexAsStr, results] = await this.redisClient.scan( nextIndex, "MATCH", "session:*", "COUNT", "100" ); nextIndex = parseInt(nextIndexAsStr, 10); results.forEach((s) => keys.add(s)); } while (nextIndex !== 0); const commands = []; keys.forEach((key) => { commands.push(["hmget", key, "userID", "username", "connected"]); }); return this.redisClient .multi(commands) .exec() .then((results) => { return results .map(([err, session]) => (err ? undefined : mapSession(session))) .filter((v) => !!v); }); } } module.exports = { InMemorySessionStore, RedisSessionStore, }; ================================================ FILE: examples/private-messaging/src/App.vue ================================================ ================================================ FILE: examples/private-messaging/src/components/Chat.vue ================================================ ================================================ FILE: examples/private-messaging/src/components/MessagePanel.vue ================================================