Redis Playground

Mono-репозиторий с тремя самостоятельными проектами, демонстрирующими различные паттерны и стратегии работы с Redis: от сессионной аутентификации и распределённых блокировок до полнотекстового поиска и конкурентного доступа к данным.

Проект Стек Назначение
redis-with-nest NestJS, ioredis, Prisma, PostgreSQL Clean Architecture, сессии, кеширование, блокировки (Redlock, WATCH, custom lock)
redis-with-svelte SvelteKit, redis v4, Tailwind Аукционная платформа RBay: поиск (RediSearch), HyperLogLog, Lua-скрипты, ставки
redis-search Vue 3, Pinia, MSW, UnoCSS Поисковый интерфейс с мок-сервером и closure-based стейт-менеджментом

Стек технологий

Технология redis-with-nest redis-with-svelte redis-search
Runtime Node.js 18 Node.js Node.js
Framework NestJS SvelteKit Vue 3.4
Redis-клиент ioredis redis v4 + ioredis
ORM / DB Prisma + PostgreSQL
Аутентификация Passport.js (session) scrypt + Keygrip
Валидация Zod
Логирование Winston + Logstash
Стилизация Tailwind CSS UnoCSS + SCSS
Тестирование Jest, Supertest
API-моки MSW 2.x
Сборка Webpack (Nest CLI) Vite (SvelteKit) Vite 5.4
Контейнеризация Docker Compose
Мониторинг ELK (Elasticsearch, Logstash, Kibana)

Структура репозитория

redis-playground/
├── package.json                  # Корневые devDependencies (msw, faker)
├── redis-with-nest/              # NestJS + Redis (Clean Architecture)
│   ├── src/
│   │   ├── main.ts
│   │   ├── modules/
│   │   │   ├── auth-session/     # Сессионная аутентификация
│   │   │   └── bids/             # Конкурентный доступ (4 стратегии блокировки)
│   │   └── shared/
│   │       ├── modules/          # redis, cache, redlock, logger, context, prisma, promise
│   │       ├── filters/          # AllExceptionFilter, PrismaExceptionFilter
│   │       ├── interceptors/     # Logging, Performance, Cache, Serializer
│   │       ├── middlewares/      # TraceId generator
│   │       ├── helpers/          # Cookies, files, formatters, transformers
│   │       └── swagger/          # Swagger config, decorators
│   ├── docker/                   # Entrypoint, ELK config
│   ├── environments/             # .env.dev, .env.prod, .env.test
│   ├── test/                     # Helpers, configs (e2e, integration, unit)
│   ├── Dockerfile                # Multi-stage build
│   └── docker-compose.local.yml
├── redis-with-svelte/            # SvelteKit + Redis (аукцион RBay)
│   ├── src/
│   │   ├── routes/               # Страницы и API-эндпоинты
│   │   ├── services/             # Redis-запросы, аутентификация, middleware
│   │   └── lib/components/       # Svelte-компоненты
│   ├── seeds/                    # Генерация и загрузка данных
│   ├── worker/                   # Фоновые задачи
│   ├── cli/                      # Redis CLI wrapper
│   └── sandbox/                  # Нагрузочное тестирование
└── redis-search/                 # Vue 3 поисковый интерфейс
    ├── src/
    │   ├── components/           # V-library, search, tags, results
    │   ├── stores/               # Closure-based Pinia stores
    │   ├── network/              # CRUD-операции с MSW
    │   ├── mocks/                # MSW handlers + Faker
    │   └── shared/               # Утилиты, composables
    └── clean-script/             # fp-ts pipeline обработки данных

redis-with-nest

Архитектура

Проект следует принципам Clean / Hexagonal Architecture: бизнес-логика изолирована от инфраструктуры, зависимости направлены внутрь.

graph TB
    subgraph Presenter["Presenter (Controller, DTO)"]
        C[AuthSessionController]
        BC[BidsController]
    end

    subgraph Application["Application (Use Cases)"]
        LU[LoginSessionUsecase]
        LO[LogoutSessionUsecase]
    end

    subgraph Domain["Domain (Entities, Repository Interface)"]
        UE[UserEntity]
        RI[IAuthSessionRepository]
    end

    subgraph Infrastructure["Infrastructure"]
        R[AuthSessionRepository]
        S[SessionRedisStrategy]
        SR[SessionRedisSerializer]
        G[Guards]
        M[Mapper]
    end

    subgraph Shared["Shared Modules"]
        Redis[RedisModule]
        Cache[CacheModule]
        Redlock[RedlockModule]
        Logger[LoggerModule]
        Context[ContextModule]
        Prisma[PrismaModule]
        Promise[PromiseModule]
    end

    C --> LU
    C --> LO
    LU --> RI
    R -.->|implements| RI
    R --> Prisma
    S --> LU
    SR --> R
    G --> S
    BC --> Redis
    BC --> Redlock

Модуль auth-session

Сессионная аутентификация с хранением сессий в Redis через connect-redis.

Поток аутентификации:

  1. POST /api/v1/auth/session/login — Passport Strategy валидирует учётные данные через LoginSessionUsecase
  2. SessionRedisSerializer.serializeUser сохраняет { email, roles } в Redis
  3. Cookie session_id устанавливается клиенту (HTTPOnly, SameSite=lax)
  4. Последующие запросы проверяются через SessionRedisGuard
  5. POST /api/v1/auth/session/logout — уничтожение сессии, очистка cookie

Слои модуля:

Слой Файлы Ответственность
Presenter auth-session.controller.ts, dto/ HTTP-эндпоинты, валидация входных данных (Zod)
Application login-session.usecase.ts, logout-session.usecase.ts Бизнес-логика: проверка пароля (bcrypt), поиск пользователя
Domain user.entity.ts, auth-session-repository.types.ts Сущность UserEntity, интерфейс репозитория
Infrastructure strategies/, guards/, repositories/, helpers/ Passport-стратегия, сериализатор, guards, репозиторий (Prisma)

DTO-валидация (Zod):

const AuthLoginSessionRequestSchema = z.object({
  email: z.string().email('Invalid email format'),
  password: z.string().min(6, 'Password should be at least 6 characters'),
}).strict()

Модуль bids

Демонстрация четырёх стратегий конкурентного доступа к Redis-данным при размещении ставок.

graph LR
    subgraph Strategies["Стратегии блокировки"]
        NL["No Lock<br/>(race condition)"]
        CL["Custom Lock<br/>(SET NX + retry)"]
        RL["Redlock<br/>(распределённый)"]
        WT["WATCH + MULTI/EXEC<br/>(оптимистичная)"]
    end

    NL -->|"все 5 ставок проходят"| BAD["Некорректно"]
    CL -->|"1-2 ставки проходят"| OK["Корректно"]
    RL -->|"только валидные"| OK
    WT -->|"ioredis limitation"| SKIP["Не реализовано"]

Custom Lock — ручная реализация поверх SET NX:

// Захват блокировки
const lockKey = `lock:${key}`
const lockValue = uuid()
await redis.set(lockKey, lockValue, { ttl: 500, skip: 'IF_EXIST' }) // NX

// Критическая секция
const item = await this.getItemById(newBid.itemId)
// ... валидация, обновление ...

// Освобождение
await redis.customUnlock(key) // DEL только если значение совпадает

Redlock — распределённая блокировка через библиотеку redlock:

const lock = await redisService.redlockLock(generateItemKey(itemId), 500)
try {
  // критическая секция
} finally {
  await lock.release()
}

Конфигурация Redlock:

new Redlock([redisConnection], {
  driftFactor: 0.01,
  retryCount: 20,
  retryDelay: 200,
  retryJitter: 200,
  automaticExtensionThreshold: 500,
})
Стратегия Эндпоинт Механизм Результат (5 конкурентных ставок)
No Lock POST /bids/make-no-lock Нет Все 5 проходят (race condition)
Custom Lock POST /bids/make1 SET NX + retry loop 1-2 успешных
Redlock POST /bids/make2 Multi-node consensus Только валидные
WATCH + Txn POST /bids/make-watch-transaction Оптимистичная блокировка Не работает с ioredis

Shared-модули

RedisModule

Обёртка над ioredis с поддержкой single/cluster режимов и встроенными механизмами блокировки.

// Основные операции
redis.get<T>(key): Promise<T | null>
redis.set<T>(key, value, options?: { ttl?, skip?: 'IF_EXIST' | 'IF_NOT_EXIST', keepTtl? }): Promise<T>
redis.del(key): Promise<void>
redis.expire(key, ttl): Promise<void>
redis.incrbyfloat(key, amount, options?): Promise<number>
redis.sadd(key, value, options?): Promise<void>
redis.smembers(key): Promise<string[]>

// Блокировки
redis.customLock(key, { ttl: 500, timeout: 5000 }): Promise<void>
redis.customUnlock(key): Promise<void>
redis.redlockLock(key, ttl): Promise<Lock>

CacheModule

Кеширование HTTP-ответов с поддержкой bucket-инвалидации и сжатия.

Формат ключа кеша:

cache:{traffic}:{hostname}:{method}:{path}:{query}/{uniqueSuffix}

Decorator-based API:

@Cache<ItemList>({
  buckets: ({ req }) => 'items',
  uniqueSuffix: 'same',
  serializable: true,
  timeout: 500,
  ttl: 60000,
})

Жизненный цикл:

  1. Interceptor проверяет кеш перед вызовом контроллера
  2. Cache hit — возврат с заголовком CacheStatus: HIT
  3. Cache miss — вызов контроллера, кеширование ответа
  4. Поддержка gzip-сжатия для больших JSON-объектов
  5. Bucket-инвалидация: все ключи группы удаляются одной операцией

Другие shared-модули

Модуль Назначение
RedlockModule Инстанс Redlock с конфигурацией drift, retry, jitter
LoggerModule Winston: уровни ERROR..TRACE, транспорты CONSOLE/FILE/LOGSTASH
ContextModule AsyncLocalStorage — request-scoped данные: traceId, метаданные, статус кеша
PromiseModule sleep, resolveOrTimeout, resolveLimited (concurrency), retryOnRejection
PrismaModule ORM-клиент, миграции, seed, утилиты очистки для тестов

Shared Helpers

Категория Функции Назначение
Cookies addCookies Установка cookie с параметрами (httpOnly, secure, sameSite)
Files archiveFiles, streamFileAsPdf, streamFileAsZip, removeFile, convertReadStreamToBuffer Работа с файлами: архивация (archiver), PDF-генерация (pdfkit), стриминг
Formatters formatMessageString, safeResolveResponse Форматирование сообщений об ошибках, безопасный разбор ответов
Transformers streamToBuffer, tryUnwindArrayToValue, unwindArrayToValue, windValueToArray Трансформация потоков и массивов
Types global.ts Глобальные типы: HttpMethod, AppRequest, AppResponse

Схема базы данных (Prisma)

model User {
  id                     Int              @id @default(autoincrement())
  email                  String?          @unique
  username               String?          @unique
  provider               AuthProviderEnum // LOCAL, GMAIL
  roles                  RoleEnum[]       @default([USER])
  password               String?
  isEmailConfirmed       Boolean          @default(false)
  emailConfirmationToken String?          @unique
  rtSessions             RTSession[]
  certificates           Certificate[]
}

model RTSession {
  id        Int      @id @default(autoincrement())
  rt        String   @unique
  rtExpDate DateTime
  userAgent String?
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    Int
  @@unique([rt, userId])
}

model Certificate {
  id     Int   @id @default(autoincrement())
  User   User  @relation(fields: [userId], references: [id])
  userId Int
  file   Bytes @db.ByteA
}

API-эндпоинты

Метод Путь Guard Описание
POST /api/v1/auth/session/login SessionRedisLoginGuard Аутентификация, создание сессии
POST /api/v1/auth/session/logout SessionRedisGuard Уничтожение сессии
GET /api/v1/auth/session/status SessionRedisGuard Проверка статуса сессии
POST /api/v1/bids/make-no-lock Ставка без блокировки
POST /api/v1/bids/make1 Ставка с custom lock
POST /api/v1/bids/make2 Ставка с Redlock
POST /api/v1/bids/make-watch-transaction Ставка с WATCH+MULTI
GET /api/v1/ Health check
POST /api/v1/cache-bucket/invalidate Инвалидация кеш-бакетов
GET /api/v1/docs Swagger UI

Interceptors, Filters, Middlewares

Жизненный цикл запроса:

Express middleware (headers) -> AsyncLocalStorage context -> Session + Passport
-> Global exception filter -> Guards -> Interceptors (log, perf, cache)
-> Pipes (Zod validation) -> Controller -> Response interceptor
Тип Класс Назначение
Filter AllExceptionFilter Глобальный перехват ошибок с маппингом TypeError, PrismaException
Interceptor PerformanceInterceptor Замер длительности запроса
Interceptor LoggerInterceptor Логирование request/response с traceId
Interceptor CacheInterceptor Чтение/запись кеша, bucket-управление
Interceptor ZodSerializerInterceptor Сериализация ответов через Zod-схемы
Middleware GeneratorTraceIdMiddleware Генерация уникального traceId для каждого запроса

Docker-инфраструктура

docker-compose.local.yml — 5 сервисов:

Сервис Образ Порт Назначение
app node:18-alpine (development) 3000 NestJS backend
db postgres 5432 PostgreSQL
pgadmin dpage/pgadmin4 5050 GUI для PostgreSQL
redis redis 6379 Redis
redis-commander rediscommander 8081 GUI для Redis

ELK-стек (опционально): Elasticsearch (9200), Logstash (5044), Kibana (5601).

Конфигурация Logstash pipeline (docker/elk-config/logstash/pipeline/logstash.conf):

input {
  tcp { port => 5044; codec => json_lines }
}
output {
  elasticsearch {
    hosts => "elasticsearch:9200"
    index => "nestjs-logs-%{+YYYY.MM.dd}"
  }
}

Winston отправляет логи в Logstash через winston-logstash, Kibana визуализирует их с фильтрацией по traceId, severity, timestamp.

Dockerfile (multi-stage):

development -> build -> production
  • Development: pnpm install, prisma:generate, entrypoint.sh
  • Production: только dist/, node_modules/ (prod), schema.prisma

Тестирование

Конфигурации:

Тип Конфиг Паттерн файлов
E2E jest-e2e.config.ts *.e2e-spec.ts
Integration jest-integration.config.ts *.integration-spec.ts
Unit jest-unit.config.ts *.unit-spec.ts

E2E-тесты auth-session (требуют Docker с PostgreSQL + Redis):

  1. Неверный email/пароль — 401, нет cookie
  2. Успешный логин — 200, cookie session_id
  3. Защищённый эндпоинт — 200 с данными сессии
  4. Logout — 204, сессия уничтожена
  5. Запрос после logout — 403

E2E-тесты bids (5 конкурентных ставок):

  • No lock: все 5 проходят (демонстрация race condition)
  • Custom lock: сериализация, 1-2 успешных
  • Redlock: корректная работа распределённой блокировки

Переменные окружения

PORT=3000
HOSTNAME=0.0.0.0
NODE_ENV=development
CORS_WHITELIST="http://localhost:3000,http://localhost:5000"

# Logger
TRANSPORT_LEVELS=CONSOLE,FILE
MAXIMUM_LOG_LEVEL=trace

# PostgreSQL
DB_URI=postgresql://postgres:password@db:5432/somedb?schema=public

# Redis
REDIS_HOST=redis
REDIS_PORT=6379
SESSION_SECRET=session-secret-token

# JWT
AT_SECRET=at-secret
RT_SECRET=rt-secret

redis-with-svelte

Назначение

RBay — аукционная платформа на SvelteKit с Redis в качестве единственного хранилища данных. Все операции (пользователи, товары, ставки, лайки, просмотры, сессии, поиск, кеширование страниц) реализованы через различные структуры данных Redis.

Типы данных

interface Item {
  id: string; name: string; ownerId: string; imageUrl: string;
  description: string; createdAt: DateTime; endingAt: DateTime;
  views: number; likes: number; price: number; bids: number;
  highestBidUserId: string;
}

interface User { id: string; username: string; password: string; }
interface Session { id: string; userId: string | null; username: string; }
interface Bid { createdAt: DateTime; amount: number; }

Key-функции (services/keys.ts)

Централизованные функции генерации Redis-ключей:

Функция Ключ Тип
usersKey(id) users#${id} HASH
usernamesKey() usernames ZSET
usernamesUniqueKey() usernames:unique SET
itemsKey(id) items#${id} HASH
itemsByViewsKey() items:views ZSET
itemsByPriceKey() items:price ZSET
itemsByEndingAtKey() items:endingAt ZSET
itemsViewsKey(id) items:views#${id} HLL
bidHistoryKey(id) history#${id} LIST
userLikesKey(id) users:likes#${id} SET
sessionsKey(id) sessions#${id} HASH
pageCacheKey(route) pagecache#${route} STRING
itemsIndexKey() idx:items Search Index

Модель данных Redis

graph TD
    subgraph Hash["HASH"]
        U["users#{id}<br/>username, password"]
        I["items#{id}<br/>name, description, imageUrl,<br/>ownerId, price, bids,<br/>views, likes, endingAt"]
        S["sessions#{id}<br/>userId, username"]
    end

    subgraph SortedSet["SORTED SET"]
        UN["usernames<br/>username -> score(hexId)"]
        IP["items:price<br/>itemId -> price"]
        IV["items:views<br/>itemId -> viewCount"]
        IE["items:endingAt<br/>itemId -> endingTime"]
    end

    subgraph Set["SET"]
        UU["usernames:unique<br/>{username}"]
        UL["users:likes#{userId}<br/>{itemId, ...}"]
    end

    subgraph List["LIST"]
        BH["history#{itemId}<br/>[amount:timestamp, ...]"]
    end

    subgraph HyperLogLog["HYPERLOGLOG"]
        HLL["items:views#{itemId}<br/>уникальные userId"]
    end

    subgraph Search["REDIS SEARCH"]
        IDX["idx:items<br/>name(TEXT), description(TEXT),<br/>ownerId(TAG), price(NUMERIC),<br/>views(NUMERIC), likes(NUMERIC)"]
    end

    subgraph String["STRING"]
        PC["pagecache#{route}<br/>HTML (TTL 2s)"]
        LK["lock:{key}<br/>token (PX 2000)"]
    end

Индекс idx:items создаётся при старте приложения:

FT.CREATE idx:items ON HASH PREFIX 1 items#
  SCHEMA
    name TEXT SORTABLE
    description TEXT
    ownerId TAG
    endingAt NUMERIC SORTABLE
    bids NUMERIC SORTABLE
    views NUMERIC SORTABLE
    price NUMERIC SORTABLE
    likes NUMERIC SORTABLE

Поисковый запрос:

// Очистка термина, добавление wildcards
// "red chair" -> "%red%|%chair%"
const cleaned = term.replace(/[^a-zA-Z0-9 ]/g, '').trim()
const words = cleaned.split(' ').map(w => `%${w}%`).join('|')

// Поиск с весами: name x5, description x1
FT.SEARCH idx:items words LIMIT 0 5

Lua-скрипты

Три кастомных Lua-скрипта для атомарных операций:

unlock — атомарное освобождение блокировки:

if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
end

incrementView — атомарный учёт уникальных просмотров:

local added = redis.call('PFADD', KEYS[1], ARGV[1])  -- HyperLogLog
if added == 1 then
  redis.call('HINCRBY', KEYS[2], 'views', 1)          -- Item hash
  redis.call('ZINCRBY', KEYS[3], 1, ARGV[2])           -- Sorted set
end

Распределённые блокировки

Пессимистичная блокировка для операций со ставками:

async function withLock(key: string, callback: (signal, proxy) => Promise<any>) {
  const token = crypto.randomBytes(6).toString('hex')

  // Попытка захвата: SET NX с PX 2000
  // До 20 ретраев, 100ms между попытками
  await client.SET(`lock:${key}`, token, { NX: true, PX: 2000 })

  // Proxy-клиент с проверкой таймаута
  const proxy = buildClientProxy(client, lockTimeout)

  try {
    await callback(signal, proxy)
  } finally {
    // Атомарное освобождение через Lua-скрипт
    await client.unlock(`lock:${key}`, token)
  }
}

Аутентификация

Реализация без внешних библиотек:

Этап Механизм
Хеширование пароля crypto.scrypt(password, salt, 32) — 4-байтный salt
Хранение users#{id} HASH: {username, password: "hash.salt"}
Уникальность usernames:unique SET + usernames ZSET (score = parseInt(id, 16))
Сессии sessions#{id} HASH: {userId, username}
Подпись cookie Keygrip HMAC-SHA1, формат: auth=sessionId:signature
TTL cookie 1 год

Маршрутизация SvelteKit

Метод Путь Описание
GET / Главная: ending soonest, most views, highest price
GET /items/[id] Страница товара: incrementView, bid history, likes
GET /items/search?term= Полнотекстовый поиск (FT.SEARCH)
POST /items/[id]/bids Новая ставка (withLock)
POST /items/[id]/likes Лайк товара
DELETE /items/[id]/likes Удаление лайка
GET /users/[id] Профиль: общие лайки (SINTER), избранное
GET /dashboard/items Товары пользователя (FT.SEARCH @ownerId)
POST /dashboard/items/new Создание нового товара
POST /auth/signup Регистрация
POST /auth/signin Вход
POST /auth/signout Выход
GET /sessions Текущая сессия
GET /about, /privacy Статические страницы (кешируются 2s)

Middleware-цепочка

hooks.ts: useErrors -> useCachePage -> useSession
Middleware Назначение
useErrors Глобальный перехват ошибок, ответ 500 с JSON
useCachePage Кеширование статических страниц в Redis (TTL 2s)
useSession Создание/загрузка сессии, подпись cookie через Keygrip

Сидирование данных

seeds/gen.js генерирует content.json:

  • 100 пользователей с уникальными ID (6-char hex)
  • ~50 товаров (мебель) с random owner, длительностью 1-12 часов
  • 1600 ставок с увеличивающейся ценой
  • 800 лайков (без дубликатов)
  • 2500 просмотров (без дубликатов)

seeds/seed.js загружает данные в Redis:

  1. FLUSHALL
  2. Для каждого товара: HSET, ZADD (views, price, endingAt)
  3. Для каждого пользователя: HSET, ZADD (usernames), SADD (unique)
  4. Для каждой ставки: RPUSH (history), ZINCRBY (bids, price)
  5. Лайки: SADD (users:likes)
  6. Просмотры: PFADD (HyperLogLog)

Frontend-компоненты

Компонент Назначение
header.svelte Навигация, поиск, auth-состояние
search.svelte Debounced-поиск (300ms) с dropdown
card.svelte Карточка товара (цена, время, просмотры)
carousel.svelte Горизонтальная прокрутка карточек
chart.svelte Chart.js — линейный график истории ставок
table.svelte Пагинированная таблица товаров с сортировкой
like-button.svelte Кнопка лайка с счётчиком
stat.svelte Виджет статистики (label + value)

Worker / Job-система

Инфраструктура для фоновых задач (частично реализована):

worker/
├── client.ts          # Отдельный Redis-клиент для воркера
├── types.ts           # JobSpec, ErroredJobSpec, CompletedJobSpec, WrappedMessage
└── jobs/
    └── remove-item.ts # Stub: удаление просроченных товаров

Типы:

interface JobSpec { name: string; runAt: DateTime; args: any }
interface WrappedMessage { id: string; job: JobSpec; attempts: number }
// Ключи: groupName, delayedKey, activeKey, failedKey, completedKey

Запуск: pnpm run worker. Воркер подключается к Redis с теми же параметрами, что и основное приложение.

CLI

cli/index.js — обёртка над redis-cli, автоматически подставляющая параметры подключения из .env:

pnpm run cli
# Эквивалентно: redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PW

Sandbox

sandbox/index.ts — нагрузочное тестирование ставок:

  • 50 конкурентных ставок на один товар
  • Распределение по 3 серверным портам (3000-3002)
  • Метрики: время выполнения, успехи/ошибки, процент успеха

redis-search

Назначение

Поисковый интерфейс на Vue 3 с мок-сервером (MSW). Фокус на компонентной архитектуре, кастомном стейт-менеджменте (closure-based Pinia) и стилизации (UnoCSS + SCSS).

Компонентная архитектура

TheGlobalWrapper (CSS Grid 3x6)
|-- TheSearch (row 1, cols 1-3)
|   |-- TheSearchInput
|   |-- TheSearchBtn -> VToggleButton
|   |-- TheAddElementBtn
|   +-- TheAddElementModal
|       |-- VModal (teleport to body)
|       |-- BrandNewTag
|       +-- TheAddElementModalBtn
|-- TheTags (rows 2-6, col 1)
|   |-- TheTag -> VCheckbox[]
|   +-- TheTagSearchBtn -> VToggleButton
+-- TheResults (rows 2-6, cols 2-3)
    |-- VCubeLoader (loading state)
    +-- SearchResult[] (результаты)

V-компонентная библиотека

Набор переиспользуемых UI-компонентов (src/components/V/):

Компонент Props Особенности
VModal isOpen, openModal, closeModal, width, innerRenderKey Teleport to body, overlay click закрывает, slots: close-button, content, z-index 99999
VCheckbox v-model:isChecked, name Кастомный чекбокс с анимацией scale, emit { value, name }
VToggleButton isActive, color, text Динамические CSS-классы по цвету (teal/blue/purple/...), active/disabled состояния
VCubeLoader 3D CSS-куб с вращением (preserve-3d), пульсирующая сфера, используется при загрузке

Closure-based stores

Вместо стандартного defineStore Pinia используется паттерн closure-factory:

// stores/active-search.ts
function useActiveSearchStoreClosure() {
  const searchVal = ref('')
  const isLocked = ref(false)

  // Без аргументов — readonly
  // С аргументом — writable (инициализация)
  return function (isLockedInitial?: boolean) {
    if (isLockedInitial !== undefined) {
      isLocked.value = isLockedInitial
      return { searchVal, isLocked } as TSearchStoreManager
    }
    return {
      searchVal: readonly(searchVal),
      isLocked: readonly(isLocked),
    } as TSearchStoreReadonly
  }
}

export const useActiveSearchStoreShared = useActiveSearchStoreClosure()

active-tags store — аналогичный паттерн с ShallowRef для массива тегов:

const initTagNames = shallowRef<{ name: string; isChecked: boolean }[]>([])

function updateCheckboxState({ value, name }: { value: boolean; name: string }) {
  const tag = initTagNames.value.find(t => t.name === name)
  if (tag) tag.isChecked = value
}

full-search store — оркестратор:

function useFullSearch(searchedData: Ref<TAddElement[]>, isLoading: Ref<boolean>) {
  const activeSearchData = useActiveSearchStoreShared()  // readonly
  const activeTagsData = useActiveTagsStoreShared()      // readonly

  const debouncedSearch = debounce(async () => {
    const searchElement = fullSearchNetworkAdapter({ activeSearchData, activeTagsData })
    const results = await makeSearch(searchElement, { isLoading })
    if (results) searchedData.value = results
  }, 1000)

  watch([activeSearchData, activeTagsData], debouncedSearch, { deep: true })
}

Network-слой с MSW

Архитектура:

Components -> Stores -> fullSearchNetworkAdapter -> makeSearch -> MSW Handler -> Faker

MSW Handler:

http.post(`${BACKEND_URL}/element`, async ({ request }) => {
  const body = await readStreamAsJson(request.body.getReader())
  return HttpResponse.json(repeatInvokes(generateElement, 10))
})

CRUD-операции:

Функция Метод Путь
addElement POST /element
updateElement PATCH /element
deleteElement DELETE /element
makeSearch POST /element

Стилизация

  • UnoCSS: atomic-утилиты, кастомные shortcuts (btn, icon-btn), attributify mode
  • SCSS-модули: _reset.scss (сброс), _palette.scss (CSS-переменные для цветов), _animation.scss (transitions: glide, disclosure, modal), _shadow.scss (тени карточек)
  • CSS-переменные: 10+ цветовых шкал (50-900), темная/светлая тема
  • Breakpoints: xsm 375px, sm 640px, md 768px, lg 1024px, xl 1280px

Сборка (Vite)

// vite.config.ts
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    UnoCSS({ configFile: './uno.config.ts' }),
    nodePolyfills(),
  ],
  resolve: {
    alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
  },
})
  • @vitejs/plugin-vue — поддержка Vue 3 SFC
  • vite-plugin-vue-devtools — DevTools для отладки
  • unocss/vite — интеграция UnoCSS
  • vite-plugin-node-polyfills — полифилы Node.js для браузера (stream, buffer)
  • Алиас @ -> ./src
  • TypeScript: strict mode, noUncheckedIndexedAccess: true

Clean-script

Утилита обработки данных через fp-ts pipeline:

const pipeline = flow(readRawArr, trimAndLower, removeDuplicates, sortByAsc, saveOutput)
pipeline(path.join(__dirname, './raw.json'))

raw.json -> trim/lowercase -> unique (remeda) -> sort -> output.json


Паттерны работы с Redis

Кеширование

Проект Механизм TTL Особенности
redis-with-nest CacheInterceptor + decorator Настраиваемый gzip-сжатие, bucket-инвалидация, timeout
redis-with-svelte pagecache#{route} STRING 2s Только статические страницы

Сессии

Проект Механизм Хранение
redis-with-nest express-session + connect-redis HASH: email, roles
redis-with-svelte Ручная реализация + Keygrip HASH: userId, username

Распределённые блокировки

Стратегия Команда Гарантии Применение
SET NX (custom) SET key token NX PX ttl Single-node redis-with-nest (make1), redis-with-svelte (withLock)
Redlock Multi-node consensus Distributed redis-with-nest (make2)
WATCH/MULTI/EXEC Оптимистичная CAS redis-with-nest (не работает с ioredis)

Полнотекстовый поиск (RediSearch)

Используется в redis-with-svelte:

FT.CREATE idx:items ON HASH PREFIX 1 items#
  SCHEMA name TEXT SORTABLE description TEXT ownerId TAG ...

FT.SEARCH idx:items "%term%" LIMIT 0 5

Поиск по имени товара (weight 5) и описанию (weight 1), wildcard-подстановки, TAG-фильтрация по ownerId.

HyperLogLog

Подсчёт уникальных просмотров товаров без хранения полного множества:

PFADD items:views#{itemId} userId  -> 1 (новый) / 0 (повтор)

Атомарность через Lua-скрипт: PFADD + условный HINCRBY + ZINCRBY.

Lua-скрипты

Скрипт Проект Назначение
unlock redis-with-svelte Атомарное освобождение блокировки (GET + сравнение + DEL)
incrementView redis-with-svelte PFADD + HINCRBY + ZINCRBY в одной транзакции
addOneAndStore redis-with-svelte Атомарный инкремент и сохранение

Транзакции WATCH/MULTI/EXEC

Демонстрация оптимистичной блокировки в redis-with-nest:

await redis.watch(itemKey)
const item = await redis.get(itemKey)
// ... валидация ...
const result = await redis.multi().set(itemKey, updatedItem).exec()
// result === null -> ключ был изменён другим клиентом

Ограничение: ioredis требует изолированного выполнения, что делает этот подход неприменимым в пулированных соединениях.


Запуск и развёртывание

Требования

  • Node.js 18+
  • pnpm
  • Docker + Docker Compose (для redis-with-nest)
  • Redis 7+ с модулем RediSearch (для redis-with-svelte)

redis-with-nest

# Запуск инфраструктуры
docker compose -f docker-compose.local.yml up -d

# Генерация Prisma-клиента и миграции
pnpm run prisma:generate
pnpm run prisma:run-migration
pnpm run prisma:seed

# Development
pnpm run start:dev

# Тесты (требуют Docker)
pnpm run test:e2e:restart          # Полный цикл: rebuild + test
pnpm run test:e2e:auth-session     # Только auth-session
pnpm run test:e2e:bids             # Только bids
pnpm run test:integration          # Integration-тесты

redis-with-svelte

# Настроить .env с REDIS_HOST, REDIS_PORT
pnpm install

# Сидирование данных
pnpm run seed

# Development (порт 3000)
pnpm run dev

# Дополнительные инстансы для нагрузочного тестирования
pnpm run dev:two    # порт 3001
pnpm run dev:three  # порт 3002

# Worker
pnpm run worker

# Redis CLI
pnpm run cli

# Sandbox (нагрузочное тестирование)
pnpm run sandbox

redis-search

pnpm install

# Development
pnpm run dev

# Сборка
pnpm run build

# Линтинг
pnpm run lint
pnpm run format

Top categories

Loading Svelte Themes