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 обработки данных
Проект следует принципам 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
Сессионная аутентификация с хранением сессий в Redis через connect-redis.
Поток аутентификации:
POST /api/v1/auth/session/login — Passport Strategy валидирует учётные данные через LoginSessionUsecaseSessionRedisSerializer.serializeUser сохраняет { email, roles } в Redissession_id устанавливается клиенту (HTTPOnly, SameSite=lax)SessionRedisGuardPOST /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()
Демонстрация четырёх стратегий конкурентного доступа к 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 |
Обёртка над 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>
Кеширование 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,
})
Жизненный цикл:
CacheStatus: HIT| Модуль | Назначение |
|---|---|
| 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, утилиты очистки для тестов |
| Категория | Функции | Назначение |
|---|---|---|
| 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 |
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
}
| Метод | Путь | 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 |
Жизненный цикл запроса:
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-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
pnpm install, prisma:generate, entrypoint.shdist/, 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):
session_idE2E-тесты bids (5 конкурентных ставок):
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
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; }
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 |
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-скрипта для атомарных операций:
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 год |
| Метод | Путь | Описание |
|---|---|---|
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) |
hooks.ts: useErrors -> useCachePage -> useSession
| Middleware | Назначение |
|---|---|
useErrors |
Глобальный перехват ошибок, ответ 500 с JSON |
useCachePage |
Кеширование статических страниц в Redis (TTL 2s) |
useSession |
Создание/загрузка сессии, подпись cookie через Keygrip |
seeds/gen.js генерирует content.json:
seeds/seed.js загружает данные в Redis:
FLUSHALLHSET, ZADD (views, price, endingAt)HSET, ZADD (usernames), SADD (unique)RPUSH (history), ZINCRBY (bids, price)SADD (users:likes)PFADD (HyperLogLog)| Компонент | Назначение |
|---|---|
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/
├── 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/index.js — обёртка над redis-cli, автоматически подставляющая параметры подключения из .env:
pnpm run cli
# Эквивалентно: redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PW
sandbox/index.ts — нагрузочное тестирование ставок:
Поисковый интерфейс на 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[] (результаты)
Набор переиспользуемых 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), пульсирующая сфера, используется при загрузке |
Вместо стандартного 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 })
}
Архитектура:
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 |
btn, icon-btn), attributify mode_reset.scss (сброс), _palette.scss (CSS-переменные для цветов), _animation.scss (transitions: glide, disclosure, modal), _shadow.scss (тени карточек)// 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 SFCvite-plugin-vue-devtools — DevTools для отладкиunocss/vite — интеграция UnoCSSvite-plugin-node-polyfills — полифилы Node.js для браузера (stream, buffer)@ -> ./srcnoUncheckedIndexedAccess: trueУтилита обработки данных через 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
| Проект | Механизм | 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) |
Используется в 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.
Подсчёт уникальных просмотров товаров без хранения полного множества:
PFADD items:views#{itemId} userId -> 1 (новый) / 0 (повтор)
Атомарность через Lua-скрипт: PFADD + условный HINCRBY + ZINCRBY.
| Скрипт | Проект | Назначение |
|---|---|---|
unlock |
redis-with-svelte | Атомарное освобождение блокировки (GET + сравнение + DEL) |
incrementView |
redis-with-svelte | PFADD + HINCRBY + ZINCRBY в одной транзакции |
addOneAndStore |
redis-with-svelte | Атомарный инкремент и сохранение |
Демонстрация оптимистичной блокировки в 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 требует изолированного выполнения, что делает этот подход неприменимым в пулированных соединениях.
# Запуск инфраструктуры
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-тесты
# Настроить .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
pnpm install
# Development
pnpm run dev
# Сборка
pnpm run build
# Линтинг
pnpm run lint
pnpm run format