From 1825e563493a443039a5d0a8fa6d0b7ce3ef08b0 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:09:40 +0100 Subject: [PATCH] feat: add redis (#242) * feat: add refis * feat: add redis package Co-authored-by: Meier Lukas * feat: add example docker compose, add redis connection in package * fix: usage of client after subscribe * feat: add logger for redis * refactor: format files Co-authored-by: Meier Lukas --------- Co-authored-by: Meier Lukas --- .dockerignore | 3 +- .vscode/settings.json | 7 +- Dockerfile | 2 +- apps/nextjs/src/app/[locale]/manage/test.tsx | 27 +++++-- development.docker-compose.yml | 14 ++++ packages/api/package.json | 1 + packages/api/src/router/user.ts | 14 ++-- packages/redis/index.ts | 1 + packages/redis/package.json | 40 ++++++++++ packages/redis/src/index.ts | 39 ++++++++++ packages/redis/tsconfig.json | 8 ++ pnpm-lock.yaml | 78 ++++++++++++++++++++ scripts/run.sh | 3 + 13 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 development.docker-compose.yml create mode 100644 packages/redis/index.ts create mode 100644 packages/redis/package.json create mode 100644 packages/redis/src/index.ts create mode 100644 packages/redis/tsconfig.json diff --git a/.dockerignore b/.dockerignore index 72e9aa425..afe5fc4a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,5 @@ node_modules npm-debug.log README.md .next -.git \ No newline at end of file +.git +development.docker-compose.yml \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index dca73617a..b59ae4f97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,10 @@ } ], "typescript.tsdk": "node_modules\\typescript\\lib", - "js/ts.implicitProjectConfig.experimentalDecorators": true + "js/ts.implicitProjectConfig.experimentalDecorators": true, + "cSpell.words": [ + "superjson", + "homarr", + "trpc" + ] } diff --git a/Dockerfile b/Dockerfile index 20b3e9100..47b833985 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:20-alpine AS base FROM base AS builder -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat redis RUN apk update # Set working directory WORKDIR /app diff --git a/apps/nextjs/src/app/[locale]/manage/test.tsx b/apps/nextjs/src/app/[locale]/manage/test.tsx index abdf3a520..3fd419cf1 100644 --- a/apps/nextjs/src/app/[locale]/manage/test.tsx +++ b/apps/nextjs/src/app/[locale]/manage/test.tsx @@ -1,23 +1,38 @@ "use client"; -import { useState } from "react"; +import { useCallback, useState } from "react"; +import type { ChangeEvent } from "react"; import { clientApi } from "@homarr/api/client"; -import { Stack, Text } from "@homarr/ui"; +import { Button, Stack, Text, TextInput } from "@homarr/ui"; export const Test = () => { - const [value, setValue] = useState(0); + const [value, setValue] = useState(""); + const [message, setMessage] = useState("Hello, world!"); + const { mutate } = clientApi.user.setMessage.useMutation(); clientApi.user.test.useSubscription(undefined, { - onData(data) { - setValue(data); + onData({ message }) { + setMessage(message); }, onError(err) { alert(err); }, }); + + const onChange = useCallback( + (event: ChangeEvent) => setValue(event.target.value), + [setValue], + ); + const onClick = useCallback(() => { + mutate(value); + setValue(""); + }, [mutate, value]); + return ( - This will change after one second: {value} + + + This message gets through subscription: {message} ); }; diff --git a/development.docker-compose.yml b/development.docker-compose.yml new file mode 100644 index 000000000..9e8557808 --- /dev/null +++ b/development.docker-compose.yml @@ -0,0 +1,14 @@ +############################ +# +# This compose file is only for development. +# Do not use this in production. +# +############################ + +name: development-docker-compose +services: + redis: + container_name: redis + image: redis + ports: + - "6379:6379" \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index c107baa83..f7fc176a6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,6 +25,7 @@ "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/log": "workspace:^", + "@homarr/redis": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@trpc/client": "next", "@trpc/server": "next", diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 9eddcf0d0..cc199235c 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -5,6 +5,7 @@ import { createSalt, hashPassword } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { createId, eq, schema } from "@homarr/db"; import { users } from "@homarr/db/schema/sqlite"; +import { exampleChannel } from "@homarr/redis"; import { validation, z } from "@homarr/validation"; import { createTRPCRouter, publicProcedure } from "../trpc"; @@ -106,13 +107,14 @@ export const userRouter = createTRPCRouter({ }) .where(eq(users.id, input.userId)); }), + setMessage: publicProcedure.input(z.string()).mutation(async ({ input }) => { + await exampleChannel.publish({ message: input }); + }), test: publicProcedure.subscription(() => { - return observable((emit) => { - let counter = 0; - setInterval(() => { - counter = counter + 1; - emit.next(counter); - }, 1000); + return observable<{ message: string }>((emit) => { + exampleChannel.subscribe((message) => { + emit.next(message); + }); }); }), }); diff --git a/packages/redis/index.ts b/packages/redis/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/redis/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/redis/package.json b/packages/redis/package.json new file mode 100644 index 000000000..aed63cce6 --- /dev/null +++ b/packages/redis/package.json @@ -0,0 +1,40 @@ +{ + "name": "@homarr/redis", + "private": true, + "version": "0.1.0", + "exports": { + ".": "./index.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "license": "MIT", + "type": "module", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "eslint .", + "format": "prettier --check . --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "ioredis": "5.3.2", + "@homarr/log": "workspace:^" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^8.57.0", + "typescript": "^5.4.3" + }, + "eslintConfig": { + "extends": [ + "@homarr/eslint-config/base" + ] + }, + "prettier": "@homarr/prettier-config" +} diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts new file mode 100644 index 000000000..a6b40e461 --- /dev/null +++ b/packages/redis/src/index.ts @@ -0,0 +1,39 @@ +import { Redis } from "ioredis"; +import superjson from "superjson"; + +import { logger } from "@homarr/log"; + +const subscriber = new Redis(); +const publisher = new Redis(); +const lastDataClient = new Redis(); + +const createChannel = (name: string) => { + return { + subscribe: (callback: (data: TData) => void) => { + void lastDataClient.get(`last-${name}`).then((data) => { + if (data) { + callback(superjson.parse(data)); + } + }); + void subscriber.subscribe(name, (err) => { + if (!err) { + return; + } + logger.error( + `Error with channel '${name}': ${err.name} (${err.message})`, + ); + }); + subscriber.on("message", (channel, message) => { + if (channel !== name) return; + + callback(superjson.parse(message)); + }); + }, + publish: async (data: TData) => { + await lastDataClient.set(`last-${name}`, superjson.stringify(data)); + await publisher.publish(name, superjson.stringify(data)); + }, + }; +}; + +export const exampleChannel = createChannel<{ message: string }>("example"); diff --git a/packages/redis/tsconfig.json b/packages/redis/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/redis/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca3eb4c62..b9790dd1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: '@homarr/log': specifier: workspace:^ version: link:../log + '@homarr/redis': + specifier: workspace:^0.1.0 + version: link:../redis '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation @@ -601,6 +604,31 @@ importers: specifier: ^5.4.3 version: 5.4.3 + packages/redis: + dependencies: + '@homarr/log': + specifier: workspace:^ + version: link:../log + ioredis: + specifier: 5.3.2 + version: 5.3.2 + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^8.57.0 + version: 8.57.0 + typescript: + specifier: ^5.4.3 + version: 5.4.3 + packages/spotlight: dependencies: '@homarr/translation': @@ -2145,6 +2173,10 @@ packages: dev: false optional: true + /@ioredis/commands@1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4782,6 +4814,11 @@ packages: engines: {node: '>=6'} dev: false + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -6835,6 +6872,23 @@ packages: loose-envify: 1.4.0 dev: false + /ioredis@5.3.2: + resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -7384,10 +7438,18 @@ packages: dependencies: p-locate: 5.0.0 + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + /lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} dev: true + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -8868,6 +8930,18 @@ packages: resolve: 1.22.8 dev: true + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + /reflect-metadata@0.2.1: resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} @@ -9389,6 +9463,10 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} diff --git a/scripts/run.sh b/scripts/run.sh index e922c1917..6c0aaa7d0 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,6 +1,9 @@ # Run migrations node ./db/migrate.mjs ./db/migrations +# Start Redis +redis-server & + # Run the nestjs backend node apps/nestjs/dist/main.mjs &