diff --git a/README.md b/README.md index 255262c..e41f475 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,21 @@ We have grouped the samples into multiple groups #### JavaScript / TypeScript -| Name | Stack | Description | Preview | -| --------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| [Astro](./astro/README.md) | [Astro](https://astro.build/), [Nginx](https://nginx.org/) | A simple Astro generated static website hosted on lttle.cloud | [Link](https://astro--samples--public--aifrim.eu.lttle.host/) | -| [Next.js App](./nextjs-app/README.md) | [Next.js](https://nextjs.org/), [Drizzle ORM](https://orm.drizzle.team/), [PostgreSQL](https://postgresql.org/) | A simple CRUD Next.js application hosted on lttle.cloud | [Link](https://nextjs-app--samples--public--aifrim.eu.lttle.host/) | -| [React + Vite](./react-vite/README.md) | [React](https://react.dev/), [Vite](https://vite.dev/) | A simple client-only React application built with vite hosted on lttle.cloud | [Link](https://react-vite--samples--public--aifrim.eu.lttle.host/) | -| [Hono API](./nodejs/hono-api/README.md) | [Hono](https://hono.dev/), [Drizzle ORM](https://orm.drizzle.team/), [PostgreSQL](https://postgresql.org/) | A simple CRUD API using Hono framework hosted on lttle.cloud | [Link](https://hono-api--samples--public--aifrim.eu.lttle.host/) | +| Name | Stack | Description | Preview | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [Astro](./astro/README.md) | [Astro](https://astro.build/), [Nginx](https://nginx.org/) | A simple Astro generated static website | [Link](https://astro--samples--public--aifrim.eu.lttle.host/) | +| [Next.js App](./nextjs-app/README.md) | [Next.js](https://nextjs.org/), [Drizzle ORM](https://orm.drizzle.team/), [PostgreSQL](https://postgresql.org/) | A simple CRUD Next.js application | [Link](https://nextjs-app--samples--public--aifrim.eu.lttle.host/) | +| [React + Vite](./react-vite/README.md) | [React](https://react.dev/), [Vite](https://vite.dev/) | A simple client-only React application built with vite | [Link](https://react-vite--samples--public--aifrim.eu.lttle.host/) | +| [Hono API](./nodejs/hono-api/README.md) | [Hono](https://hono.dev/), [Drizzle ORM](https://orm.drizzle.team/), [PostgreSQL](https://postgresql.org/) | A simple CRUD API using Hono framework | [Link](https://hono-api--samples--public--aifrim.eu.lttle.host/) | +| [Websockets](./nodejs/websockets/README.md) | [Node.js](https://nodejs.org/) | A simple WebSocket server & client using native WebSocket API | [Link](https://websocket-client--samples--public--aifrim.eu.lttle.host/) | #### Python For Python samples we recommend you install [`uv`](https://docs.astral.sh/uv/) beforehand. -| Name | Stack | Description | Preview | -| ------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------- | -| [FastAPI](./python/fastapi/README.md) | [FastAPI](https://fastapi.tiangolo.com/), [Uvicorn](https://www.uvicorn.org/) | A simple FastAPI application hosted on lttle.cloud | [Link](https://fastapi--samples--public--aifrim.eu.lttle.host/) | +| Name | Stack | Description | Preview | +| ------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------- | +| [FastAPI](./python/fastapi/README.md) | [FastAPI](https://fastapi.tiangolo.com/), [Uvicorn](https://www.uvicorn.org/) | A simple FastAPI application | [Link](https://fastapi--samples--public--aifrim.eu.lttle.host/) | ### Software diff --git a/nodejs/websockets/README.md b/nodejs/websockets/README.md new file mode 100644 index 0000000..02c2887 --- /dev/null +++ b/nodejs/websockets/README.md @@ -0,0 +1,32 @@ +# Websockets | lttle.cloud + +This is a simple WebSocket server sample project for [lttle.cloud](https://lttle.cloud). + +It demonstrates how to deploy a WebSocket server using Node.js and Nginx on lttle.cloud's platform. + +## Running locally + +Run the following command to start the WebSocket server and a static file server using Nginx: + +```bash +npm run dev +``` + +Then open your browser and navigate to `http://localhost:8000` (or the port your static file server is running on) to access the WebSocket client. + +## Deploy + +To deploy the project to lttle.cloud, run: + +```bash +lttle deploy websockets.lttle.yaml +``` + +This will build two Docker images + +1. One for the WebSocket server +2. One for the Nginx server to serve the static files that when accessed will connect to the WebSocket server. + +After that you can access your WebSocket client & server at `https://websocket-server--samples--public--your-tenant.eu.lttle.host/` where `your-tenant` is your lttle.cloud tenant name. + +You can check our deployment of this sample [https://websocket-server--samples--public--aifrim.eu.lttle.host/](https://websocket-server--samples--public--aifrim.eu.lttle.host/). diff --git a/nodejs/websockets/package-lock.json b/nodejs/websockets/package-lock.json new file mode 100644 index 0000000..d05f6cc --- /dev/null +++ b/nodejs/websockets/package-lock.json @@ -0,0 +1,623 @@ +{ + "name": "websockets", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "websockets", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/ws": "^8.18.1", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^22.18.13", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.18.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", + "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/nodejs/websockets/package.json b/nodejs/websockets/package.json new file mode 100644 index 0000000..9daebab --- /dev/null +++ b/nodejs/websockets/package.json @@ -0,0 +1,24 @@ +{ + "name": "websockets", + "version": "1.0.0", + "description": "Websocket example for lttle.cloud", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.18.13", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "dependencies": { + "@types/ws": "^8.18.1", + "ws": "^8.18.3" + } +} diff --git a/nodejs/websockets/public/index.html b/nodejs/websockets/public/index.html new file mode 100644 index 0000000..1c81ef2 --- /dev/null +++ b/nodejs/websockets/public/index.html @@ -0,0 +1,33 @@ + + + WebSocket Sample | lttle.cloud + + + + + + +
+ lttle.cloud +
Sending a message every 10 seconds
+
Waiting 10s...
+
+ While waiting run this command to see the status of the machine. +
+ In-between messages the connection should be kept alive and the machine + put to sleep. +
+
lttle machine get --ns samples websocket-server
+
and check the status field
+
+ + diff --git a/nodejs/websockets/public/logo.svg b/nodejs/websockets/public/logo.svg new file mode 100644 index 0000000..320be6b --- /dev/null +++ b/nodejs/websockets/public/logo.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nodejs/websockets/public/script.js b/nodejs/websockets/public/script.js new file mode 100644 index 0000000..85c3019 --- /dev/null +++ b/nodejs/websockets/public/script.js @@ -0,0 +1,105 @@ +window.addEventListener("load", () => { + const msg = document.querySelector("#msg"); + + // Helper function to set text for the message display + function setText(text) { + msg.innerHTML = text; + } + + // Helper function to set CSS class for the message display + function setClass(good) { + msg.classList.remove(good ? "error" : "success"); + msg.classList.add(good ? "success" : "error"); + } + + // Helper function to clear CSS classes for the message display + function clearClass() { + msg.classList.remove("error", "success"); + } + + // Wait 10 seconds before trying to reconnect + // Or sending a new message + const DELAY = 10_000; + + function wsConnect() { + // WS_URL is injected by the server when serving the HTML file + const wsClient = new WebSocket(WS_URL); + + let messageTimeout; + + function sendMessage() { + const data = Math.ceil(10 + ((Math.random() * 1000) % 89)); + wsClient.send(`pong:${data}`); + + setText( + `Sent: pong:${data}.
Waiting for response for 20s...` + ); + + messageTimeout = setTimeout(() => { + setText(`No response from server for 2s.
Reconnecting in 2s...`); + setClass(false); + + messageTimeout = setTimeout(() => { + setText( + `Connection closed because of no response.
Reconnecting...` + ); + + setClass(false); + + wsClient.close(); + }, 2000); + }, 2 * DELAY); + } + + wsClient.onerror = (error) => { + clearTimeout(messageTimeout); + + setText(`WebSocket error.
Reconnecting in 10s...`); + setClass(false); + + console.error("WebSocket error:", error); + }; + + wsClient.onclose = () => { + clearTimeout(messageTimeout); + + setText(`WebSocket connection closed.
Reconnecting in 10s...`); + setClass(false); + + setTimeout(wsConnect, DELAY); + }; + + wsClient.onopen = () => { + clearTimeout(messageTimeout); + + setText( + "WebSocket connection opened.
Sending first message in 10s..." + ); + setClass(true); + + setTimeout(sendMessage, DELAY); + }; + + wsClient.onmessage = (event) => { + clearTimeout(messageTimeout); + + setText(`Received: ${event.data}.
Next message in 10s...`); + clearClass(); + + setTimeout(sendMessage, DELAY); + }; + } + + setTimeout(() => { + try { + wsConnect(); + } catch (error) { + setText( + `WebSocket connection failed:
${error.message} to
${wsUrl}` + ); + setClass(false); + + console.error("WebSocket connection failed:", error); + } + }, DELAY); +}); diff --git a/nodejs/websockets/public/style.css b/nodejs/websockets/public/style.css new file mode 100644 index 0000000..82d6d0a --- /dev/null +++ b/nodejs/websockets/public/style.css @@ -0,0 +1,85 @@ +html, +body { + padding: 0; + margin: 0; +} + +body { + width: 100vw; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} + +div { + font-family: monospace; + + margin: 10px; +} + +#wrapper { + text-align: center; +} + +#msg { + transition: all 0.2s ease-in-out; + + padding: 15px; + border: 1px solid; + border-radius: 5px; +} + +#cmd { + padding: 10px; + border-radius: 5px; + + display: inline-block; + + margin: 0; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #111; + color: #fff; + } + + #msg { + border-color: #666; + background-color: #000; + } + + #cmd { + background-color: #000; + } + + #msg:not(.success):not(.error) { + color: #fff; + } + + #msg.success { + color: #00aa00; + } + + #msg.error { + color: #aa0000; + } +} + +@media (prefers-color-scheme: light) { + body { + background-color: #eee; + color: #000; + } + + #msg { + border-color: #aaa; + background-color: #fff; + } + + #cmd { + background-color: #fff; + } +} diff --git a/nodejs/websockets/src/index.ts b/nodejs/websockets/src/index.ts new file mode 100644 index 0000000..ffd8c28 --- /dev/null +++ b/nodejs/websockets/src/index.ts @@ -0,0 +1,100 @@ +import { randomUUID } from "node:crypto"; +import { createReadStream, existsSync } from "node:fs"; +import { createServer } from "node:http"; +import { Transform } from "node:stream"; +import { WebSocketServer } from "ws"; + +if (process.env.WS_URL) { + console.log( + "[Server]", + `Using WebSocket URL from environment: ${process.env.WS_URL}` + ); +} else { + console.log( + "[Server]", + "No WS_URL environment variable set, using default WebSocket server." + ); +} + +const ws = new WebSocketServer({ noServer: true }); + +ws.on("connection", (socket) => { + const id = randomUUID(); + + console.log(`[${id}]`, "Client connected"); + + socket.on("message", (message) => { + console.log(`[${id}]`, `Received: ${message}`); + + console.log(`[${id}]`, `Sending: server.ping.${message}`); + socket.send(`server.ping.${message}`); + }); + + socket.on("close", () => { + console.log(`[${id}]`, "Client disconnected"); + }); + + socket.on("error", (error) => { + console.error(`[${id}]`, `WebSocket error: ${error}`); + }); +}); + +const http = createServer(async (req, res) => { + const file = req.url === "/" ? "/index.html" : req.url || ""; + + if (existsSync(`./public${file}`) === false) { + console.error("[Server]", `File not found: ./public${file}`); + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("404 Not Found"); + return; + } + + const fd = createReadStream(`./public${file}`, { encoding: "utf8" }); + + const contentType = file.endsWith(".html") + ? "text/html" + : file.endsWith(".css") + ? "text/css" + : file.endsWith(".js") + ? "application/javascript" + : file.endsWith(".svg") + ? "image/svg+xml" + : "application/octet-stream"; + + res.writeHead(200, { "Content-Type": contentType }); + + if (file.endsWith(".html")) { + console.log("[Server]", `Serving and transforming file: ./public${file}`); + + const transform = new Transform({ + transform(chunk, _encoding, callback) { + const modifiedChunk = chunk + .toString() + .replace( + process.env.WS_URL ? "wss://__WS_URL__" : "ws://__WS_URL__", + process.env.WS_URL + ? `wss://${process.env.WS_URL}` + : `ws://localhost:8000` + ); + callback(null, modifiedChunk); + }, + }); + + fd.pipe(transform).pipe(res); + } else { + console.log("[Server]", `Serving file: ./public${file}`); + fd.pipe(res); + } +}); + +http.on("upgrade", (request, socket, head) => { + ws.handleUpgrade(request, socket, head, (websocket) => { + console.log("[Server]", "Upgrading to WebSocket connection"); + ws.emit("connection", websocket, request); + }); +}); + +http.listen(8000, () => { + console.log("[Server]", "HTTP server is running on http://localhost:8000"); +}); diff --git a/nodejs/websockets/tsconfig.json b/nodejs/websockets/tsconfig.json new file mode 100644 index 0000000..cea8beb --- /dev/null +++ b/nodejs/websockets/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "module": "nodenext", + "target": "esnext", + "types": [], + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true + } +} diff --git a/nodejs/websockets/websockets.lttle.yaml b/nodejs/websockets/websockets.lttle.yaml new file mode 100644 index 0000000..99a8cda --- /dev/null +++ b/nodejs/websockets/websockets.lttle.yaml @@ -0,0 +1,19 @@ +app: + namespace: samples + name: websocket-server + build: auto + resources: + cpu: 1 + memory: 256 + mode: + flash: + timeout: 2 + strategy: + listen-on-port: 8000 + expose: + public: + port: 8000 + external: + protocol: https + environment: + WS_URL: "websocket-server--samples--public--${{ lttle.tenant }}.eu.lttle.host"