Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚀 Collaborative Todo List (Real-Time Web Application)

Тестовое задание для WIZICO TECH
Высокопроизводительное веб-приложение для совместной работы над задачами в реальном времени с поддержкой WebSocket, синхронизации присутствия, разрешения конфликтов, устойчивости к офлайну и ролевой модели доступа.


📑 Содержание

  1. Быстрый запуск (Quick Start)
  2. Архитектура и стек технологий
  3. Обоснование стратегий разрешения конфликтов
  4. Обработка Edge Cases и бизнес-правила
  5. Офлайн-устойчивость (Offline Resilience)
  6. Структура проекта
  7. Тестирование и верификация

⚡ Быстрый запуск (Quick Start)

Вариант 1: Запуск через Docker Compose (Рекомендуемый)

Для запуска всего стека (Backend + Frontend + База данных) одной командой:

docker compose up --build

Вариант 2: Локальный запуск для разработки

1. Backend (NestJS):

cd backend
npm install
npm run db:setup      # Генерирует Prisma client, накатывает схему и сидирует пользователей
npm run start:dev     # Запуск на http://localhost:3001

2. Frontend (Next.js App Router):

cd frontend
npm install
npm run dev           # Запуск на http://localhost:3000

👥 Предустановленные тестовые пользователи (Demo Users)

В базе уже созданы 2 пользователя и совместный список задач "Team Launch Sprint":

Пользователь Email Пароль Роль в списке Цвет индикатора
Alice (Admin) alice@example.com password123 ADMIN (Владелец) 🔵 Синий (#3B82F6)
Bob (Member) bob@example.com password123 MEMBER (Участник) 🟢 Зеленый (#10B981)

💡 Удобное тестирование 2 пользователей:

  1. В интерфейсе приложения в шапке (Header) и на странице логина доступен 1-Click Fast Switcher, позволяющий мгновенно переключаться между Alice и Bob.
  2. Для проверки синхронизации в реальном времени откройте два окна браузера (например, обычное окно с Alice и окно инкогнито с Bob).

🛠 Архитектура и стек технологий

┌──────────────────────────────────────┐       WebSocket / Socket.IO       ┌──────────────────────────────────────┐
│       Frontend (Next.js 14)          │ ◄───────────────────────────────► │         Backend (NestJS 10)          │
│  - App Router (React 18 + TS)        │                                   │  - WebSocket Gateway (Socket.IO)     │
│  - Tailwind CSS + Lucide Icons       │          HTTP REST (Auth)         │  - JWT Authentication Strategy       │
│  - @hello-pangea/dnd (Drag & Drop)   │ ────────────────────────────────► │  - Presence & Lock Service           │
│  - Optimistic State + Offline Queue  │                                   │  - Prisma ORM (SQLite / Postgres)    │
└──────────────────────────────────────┘                                   └──────────────────────────────────────┘
  • Backend: NestJS 10, TypeScript, @nestjs/websockets, @nestjs/platform-socket.io, @nestjs/jwt, passport-jwt, bcryptjs, Prisma ORM.
  • Frontend: Next.js 14 (App Router), TypeScript, Tailwind CSS, @hello-pangea/dnd, socket.io-client, Lucide Icons.
  • База данных: SQLite (по умолчанию для моментального локального запуска без внешних сервисов) / PostgreSQL-совместимая схема через Prisma.

🧠 Обоснование стратегий разрешения конфликтов

1. Одновременное редактирование текста одной задачи (Concurrent Text Edit)

Сравнение стратегий:

Стратегия Плюсы Минусы Вердикт для Todo List
CRDT (Yjs, Automerge) Посимвольное слияние без конфликтов Высокая сложность, большой размер метаданных на каждый символ, избыточно для коротких заголовков Избыточно
Operational Transformation (OT) Точное посимвольное слияние (Google Docs) Требует сложного централизованного сервера трансформаций, хрупкий протокол Избыточно
Last-Write-Wins (LWW) без версионирования Простота реализации Тихая потеря данных (Silent Overwrite) одного из пользователей Неприемлемо
Versioned Optimistic Concurrency + Non-destructive Merge UI (✅ Выбранная стратегия) 100% гарантия от тихой потери данных, простота и масштабируемость, прозрачный UX Требует UI-диалога/уведомления при редком прямом конфликте Идеально

Архитектурное решение:

  1. Каждая задача имеет целочисленный счетчик version.
  2. При отправке изменений клиент передает expectedVersion.
  3. Сервер атомарно сверяет версии:
    • Если версии совпадают: версия инкрементируется (version + 1), данные сохраняются и транслируются остальным участникам комнаты через событие task_updated.
    • Если обнаружена конкурентная модификация (версия на сервере изменилась): сервер возвращает статус conflict: true со свежим состоянием задачи (serverTask).
  4. На клиенте открывается Conflict Resolution Modal, где пользователь видит обе версии (свою локальную и версию коллеги) и в 1 клик выбирает: «Принять версию коллеги» или «Оставить свою версию».

2. Одновременное перетаскивание задач (Concurrent Drag & Drop Reordering)

Проблема целочисленных индексов:

Если использовать целочисленные position: 0, 1, 2, ..., то одновременное перемещение двух элементов требует массового обновления (UPDATE ... WHERE position >= X), что приводит к race conditions, дедлокам и сбиванию порядка.

Архитектурное решение (Fractional Indexing):

  • Каждая задача хранит вещественный ранг order: Float (например, 1000.0, 2000.0, 3000.0).
  • При перемещении задачи между двумя элементами с рангами $A$ и $B$, новый ранг вычисляется как среднее арифметическое: $$\text{newOrder} = \frac{A + B}{2}$$
  • При перемещении в самый верх: $\text{newOrder} = \frac{\text{nextOrder}}{2}$.
  • При перемещении в самый низ: $\text{newOrder} = \text{prevOrder} + 1000.0$.
  • Результат: операция перемещения изолирована и обновляет только одну строку ($O(1)$) без необходимости блокировок таблицы и сдвига остальных задач.

🛡 Обработка Edge Cases и бизнес-правила

1. Конфликт «Удаление vs Редактирование» (Delete vs Edit Conflict)

  • Presence-Aware Warning: Сервис PresenceService отслеживает, на какой задаче сфокусирован каждый пользователь в реальном времени (activeTaskId).
  • Если Alice редактирует задачу, а Bob нажимает «Удалить»:
    1. Frontend / Backend перехватывают попытку и открывают модальное окно:
      «Alice в данный момент редактирует эту задачу! Удаление приведет к потере ее изменений. Вы уверены, что хотите принудительно удалить?»
    2. Если удаление подтверждено: у Alice задача плавно удаляется, а всплывающий Toast информирует: «Задача была удалена пользователем Bob».

2. Права и бизнес-правила (RBAC)

Строго валидируются как на клиенте, так и на бэкенде в TasksService с возвратом типизированных ошибок:

Правило Реализация
Admin может удалить любую задачу Проверяется роль в списке (ListMember.role === 'ADMIN'). Разрешено.
Member может удалить только свою задачу Проверяется task.createdBy === userId. При попытке удалить чужую задачу сервер выбрасывает 403 Forbidden (MEMBER_NOT_CREATOR).
Нельзя удалить выполненную задачу Если task.completed === true, обычному участнику удаление блокируется с требованием сначала снять отметку выполнения.
Admin может удалить выполненную задачу (с подтверждением) Если task.completed === true и пользователь Admin, требуется явный флаг adminConfirmed: true. В UI автоматически открывается модальное окно подтверждения.

📡 Офлайн-устойчивость (Offline Resilience)

  1. Индикация статуса: При разрыве соединения шапка приложения переключается в режим 🟡 Offline (N queued).
  2. Оптимистичные обновления (Optimistic UI): Пользователь может продолжать создавать, редактировать, отмечать, перетаскивать и удалять задачи локально.
  3. Очередь мутаций (offlineQueue): Все действия сохраняются в локальную очередь (localStorage) с уникальными идентификаторами clientMutationId и временными метками.
  4. Автоматическая синхронизация при восстановлении: При событии connect клиент отправляет накопленный батч мутаций на шлюз (sync_offline_queue), сервер последовательно применяет изменения и возвращает синхронизированный снимок списка.

📂 Структура проекта

collaborative-todo/
├── backend/
│   ├── prisma/
│   │   ├── schema.prisma            # Схема БД (User, List, ListMember, Task)
│   │   └── seed.ts                  # Сидирование Alice & Bob и стартового списка
│   ├── src/
│   │   ├── auth/                    # JWT авторизация, Guard'ы, декораторы
│   │   ├── lists/                   # Управление списками и приглашениями
│   │   ├── tasks/                   # CRUD задач, fractional reorder, бизнес-валидации
│   │   ├── events/                  # Socket.IO Gateway, Presence Service
│   │   ├── prisma/                  # Prisma Module & Service
│   │   ├── app.module.ts
│   │   └── main.ts
│   ├── Dockerfile
│   └── package.json
│
├── frontend/
│   ├── src/
│   │   ├── app/
│   │   │   ├── page.tsx             # Dashboard со списками задач
│   │   │   ├── login/page.tsx       # Страница авторизации и 1-Click Switcher
│   │   │   ├── lists/[id]/page.tsx  # Основная доска списка с Drag & Drop и Presence
│   │   │   └── join/[token]/page.tsx# Присоединение по инвайт-ссылке
│   │   ├── components/              # Header, TaskItem, PresenceBar, Modals, Toast
│   │   ├── hooks/                   # useRealtimeTodoList, usePresence
│   │   ├── lib/                     # API клиент, Socket клиент, Offline Queue
│   │   └── types/                   # TypeScript интерфейсы
│   ├── Dockerfile
│   └── package.json
│
├── docker-compose.yml               # Оркестрация контейнеров
└── README.md                        # Документация проекта

🧪 Тестирование и верификация

Запуск Unit-тестов бэкенда:

cd backend
npm run test

В backend/src/tasks/tasks.service.spec.ts реализованы тесты для всех бизнес-правил:

  • Запрет обычному участнику удалять задачи, созданные другими пользователями (ForbiddenException).
  • Разрешение участнику удалять свои задачи.
  • Разрешение администратору удалять любые задачи.
  • Блокировка удаления выполненных задач обычным участником.
  • Требование подтверждения администратора при удалении выполненных задач.
  • Вычисление дробных индексов (Fractional Indexing) при drag & drop.
  • Детекция версионных конфликтов при конкурентном редактировании.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages