1101 lines
110 KiB
HTML
1101 lines
110 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru-ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Leadera — WebSocket для диаграмм Ганта — docs.a2v.space</title>
|
||
|
||
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
|
||
|
||
<link rel="stylesheet" href="../../css/custom.css">
|
||
|
||
|
||
</head>
|
||
<body>
|
||
<header class="site-header">
|
||
<nav class="navbar navbar-expand-lg">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
|
||
<div class="collapse navbar-collapse" id="navbarMain">
|
||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||
</li>
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||
</li>
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../plans/">Планы</a>
|
||
</li>
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||
</li>
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../creds/">Креды</a>
|
||
</li>
|
||
|
||
<li class="nav-item">
|
||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
<form class="d-flex search-form" role="search">
|
||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||
</form>
|
||
</div>
|
||
|
||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</nav>
|
||
|
||
|
||
<div id="search-results" class="search-results" style="display: none;">
|
||
<div class="search-results-content">
|
||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||
<h5>Результаты поиска</h5>
|
||
<ul id="results-list"></ul>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
|
||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||
<div class="sidebar-content">
|
||
|
||
|
||
|
||
<h6>Фильтры</h6>
|
||
<ul class="nav flex-column">
|
||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||
</ul>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<h6 class="mt-3">Теги</h6>
|
||
<ul class="nav flex-column">
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||
|
||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</aside>
|
||
|
||
|
||
<main class="col-md-9 col-lg-10 main-content">
|
||
|
||
<div class="article-content">
|
||
<h1>Leadera — WebSocket для диаграмм Ганта</h1>
|
||
<div class="source-path mb-3">
|
||
<small class="text-muted">
|
||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-websocket-gantt.md</code>
|
||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-websocket-gantt.md')" title="Копировать путь">
|
||
📋
|
||
</button>
|
||
</small>
|
||
</div>
|
||
|
||
|
||
|
||
<div class="tags mb-3">
|
||
|
||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||
|
||
<a href="../../tags/websocket/" class="tag">websocket</a>
|
||
|
||
<a href="../../tags/realtime/" class="tag">realtime</a>
|
||
|
||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||
|
||
<a href="../../tags/architecture/" class="tag">architecture</a>
|
||
|
||
</div>
|
||
|
||
|
||
<h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
|
||
<p><strong>Дата:</strong> 07.04.2026
|
||
<strong>Статус:</strong> Планирование
|
||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||
<hr>
|
||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
|
||
<hr>
|
||
<h2 id="2-архитектура">2. Архитектура</h2>
|
||
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
|
||
│ │
|
||
│ HTTP (мутации) │ Broadcast
|
||
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
|
||
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p>
|
||
<h3 id="комнаты">Комнаты</h3>
|
||
<p>Каждый открытый Гант — это «комната» с идентификатором <code>chartId</code>. При подключении клиент подписывается на комнату. Hub рассылает события всем подписчикам комнаты.</p>
|
||
<hr>
|
||
<h2 id="3-backend-go">3. Backend (Go)</h2>
|
||
<h3 id="31-зависимости">3.1. Зависимости</h3>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go get github.com/gorilla/websocket
|
||
</span></span></code></pre></div><h3 id="32-новые-файлы--internalws">3.2. Новые файлы — <code>internal/ws/</code></h3>
|
||
<h4 id="messagego--типы-сообщений"><code>message.go</code> — типы сообщений</h4>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">"time"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// EventType — тип события</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#66d9ef">string</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Данные</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_created"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_updated"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_deleted"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionsReordered</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"sections_reordered"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_created"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_updated"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_deleted"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Присутствие</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserJoined</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_joined"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserLeft</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_left"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Безопасность / доступ</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventAccessRevoked</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"access_revoked"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventChartArchived</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"chart_archived"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventRoleChanged</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"role_changed"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTokenExpired</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"token_expired"</span>
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Event — WS-событие, отправляемое клиентам</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#66d9ef">struct</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#e6db74">`json:"type"`</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"chartId"`</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"userId,omitempty"`</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span> <span style="color:#66d9ef">interface</span>{} <span style="color:#e6db74">`json:"payload,omitempty"`</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"timestamp"`</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// IncomingMessage — сообщение от клиента (для ping/pong)</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IncomingMessage</span> <span style="color:#66d9ef">struct</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"type"`</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h4 id="hubgo--центральный-брокер"><code>hub.go</code> — центральный брокер</h4>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Hub управляет комнатами и рассылкой событий</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Hub</span> <span style="color:#66d9ef">struct</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">RWMutex</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span> <span style="color:#75715e">// chartId → клиенты</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewHub</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Hub</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span>: make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>),
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Subscribe добавляет клиента в комнату</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] = make(<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>][<span style="color:#a6e22e">client</span>] = <span style="color:#66d9ef">true</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_joined</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserJoined</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"username"</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Username</span>},
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||
</span></span><span style="display:flex;"><span> })
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Unsubscribe удаляет клиента из комнаты</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">client</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">clients</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
|
||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>, <span style="color:#a6e22e">chartID</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_left</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserLeft</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||
</span></span><span style="display:flex;"><span> })
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Broadcast рассылает событие всем клиентам в комнате</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// SendToUser отправляет событие конкретному пользователю во всех его комнатах</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">clients</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">userID</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// broadcastExcept рассылает всем, кроме отправителя</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">sender</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">sender</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h4 id="clientgo--обёртка-ws-соединения"><code>client.go</code> — обёртка WS-соединения</h4>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"encoding/json"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">writeWait</span> = <span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pongWait</span> = <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pingPeriod</span> = (<span style="color:#a6e22e">pongWait</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">9</span>) <span style="color:#f92672">/</span> <span style="color:#ae81ff">10</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">maxMessageSize</span> = <span style="color:#ae81ff">1024</span>
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Client</span> <span style="color:#66d9ef">struct</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span> <span style="color:#66d9ef">string</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span> <span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Mutex</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClient</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">username</span>, <span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Client</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span>: <span style="color:#a6e22e">hub</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span>: <span style="color:#a6e22e">conn</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span>: <span style="color:#a6e22e">username</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span>: make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>, <span style="color:#ae81ff">64</span>),
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ReadPump читает входящие сообщения (ping/pong)</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">ReadPump</span>() {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Hub</span>.<span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ChartID</span>, <span style="color:#a6e22e">c</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||
</span></span><span style="display:flex;"><span> }()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadLimit</span>(<span style="color:#a6e22e">maxMessageSize</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetPongHandler</span>(<span style="color:#66d9ef">func</span>(<span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
|
||
</span></span><span style="display:flex;"><span> })
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">message</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">ReadMessage</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Клиент может слать только ping, данные игнорируем</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">message</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WritePump отправляет события клиенту</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">WritePump</span>() {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">NewTicker</span>(<span style="color:#a6e22e">pingPeriod</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">Stop</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||
</span></span><span style="display:flex;"><span> }()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">CloseMessage</span>, []<span style="color:#66d9ef">byte</span>{})
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">event</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">TextMessage</span>, <span style="color:#a6e22e">data</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">C</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">PingMessage</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Send ставит событие в очередь отправки</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span> <span style="color:#f92672"><-</span> <span style="color:#a6e22e">event</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">default</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Очередь полна — отключаем медленного клиента</span>
|
||
</span></span><span style="display:flex;"><span> close(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>)
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h3 id="33-ws-endpoint--internalhandlersws_handlergo">3.3. WS endpoint — <code>internal/handlers/ws_handler.go</code></h3>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">handlers</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"net/http"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/middleware"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/ws"</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gin-gonic/gin"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/google/uuid"</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">upgrader</span> = <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Upgrader</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ReadBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">WriteBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CheckOrigin</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> }, <span style="color:#75715e">// nginx проксирует</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WSHandler</span> <span style="color:#66d9ef">struct</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">WSHandler</span>{<span style="color:#a6e22e">hub</span>: <span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">ganttMW</span>: <span style="color:#a6e22e">ganttMW</span>}
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span>) <span style="color:#a6e22e">HandleGanttWS</span>(<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">Context</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 1. Получить пользователя из контекста (auth middleware уже отработал)</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">exists</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">"userID"</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">exists</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">401</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"unauthorized"</span>})
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 2. Проверить доступ к chartId (gantt middleware)</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Param</span>(<span style="color:#e6db74">"chartId"</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">chartID</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">400</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"invalid chart id"</span>})
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// TODO: проверить через ganttMW что userID имеет доступ к этому chart</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 3. Upgrade до WebSocket</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">upgrader</span>.<span style="color:#a6e22e">Upgrade</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Writer</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Request</span>, <span style="color:#66d9ef">nil</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 4. Создать клиент и подписать на комнату</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewClient</span>(
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">conn</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>.(<span style="color:#66d9ef">string</span>),
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#e6db74">"username"</span>),
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 5. Запустить pump'ы</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WritePump</span>()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">ReadPump</span>()
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h3 id="34-регистрация-маршрута">3.4. Регистрация маршрута</h3>
|
||
<p>В <code>cmd/server/main.go</code> или где регистрируются routes:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// После создания hub</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewHub</span>()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WS endpoint</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">wsHub</span>, <span style="color:#a6e22e">ganttMW</span>)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">GET</span>(<span style="color:#e6db74">"/ws/gantt/:chartId"</span>, <span style="color:#a6e22e">authMW</span>.<span style="color:#a6e22e">RequireAuth</span>(), <span style="color:#a6e22e">wsHandler</span>.<span style="color:#a6e22e">HandleGanttWS</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Передать hub в ganttHandler для broadcast'ов</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ganttHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewGanttHandler</span>(<span style="color:#a6e22e">ganttService</span>, <span style="color:#a6e22e">sectionService</span>, <span style="color:#a6e22e">taskService</span>, <span style="color:#a6e22e">blockService</span>, <span style="color:#a6e22e">wsHub</span>)
|
||
</span></span></code></pre></div><h3 id="35-интеграция-broadcast-в-handlers">3.5. Интеграция broadcast в handlers</h3>
|
||
<p>В каждый write-handler — одна строка после успешной операции:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Пример: после успешного UpdateTask</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventTaskUpdated</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#a6e22e">updatedTask</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||
</span></span><span style="display:flex;"><span>})
|
||
</span></span></code></pre></div><p>Точки интеграции в <code>gantt_handler.go</code>:</p>
|
||
<ul>
|
||
<li><code>CreateGanttChart</code> → не нужен (пользователь только что создал, он один)</li>
|
||
<li><code>UpdateGanttChart</code> / <code>ArchiveGanttChart</code> → <code>chart_archived</code></li>
|
||
<li><code>CreateSection</code> → <code>section_created</code></li>
|
||
<li><code>UpdateSection</code> → <code>section_updated</code></li>
|
||
<li><code>DeleteSection</code> → <code>section_deleted</code></li>
|
||
<li><code>ReorderSections</code> → <code>sections_reordered</code></li>
|
||
<li><code>CreateTask</code> → <code>task_created</code></li>
|
||
<li><code>UpdateTask</code> → <code>task_updated</code></li>
|
||
<li><code>DeleteTask</code> → <code>task_deleted</code></li>
|
||
</ul>
|
||
<h3 id="36-интеграция-событий-безопасности">3.6. Интеграция событий безопасности</h3>
|
||
<p>В соответствующих service-методах:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// При исключении из space (SpaceService.RemoveMember)</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">removedUserID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventAccessRevoked</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"spaceId"</span>: <span style="color:#a6e22e">spaceID</span>},
|
||
</span></span><span style="display:flex;"><span>})
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При архивации ганта</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventChartArchived</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||
</span></span><span style="display:flex;"><span>})
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При изменении роли</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventRoleChanged</span>,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"role"</span>: <span style="color:#a6e22e">newRole</span>},
|
||
</span></span><span style="display:flex;"><span>})
|
||
</span></span></code></pre></div><hr>
|
||
<h2 id="4-frontend-angular">4. Frontend (Angular)</h2>
|
||
<h3 id="41-websocket-сервис--gantt-wsservicets">4.1. WebSocket сервис — <code>gantt-ws.service.ts</code></h3>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// src/app/pages/gantt/services/gantt-ws.service.ts
|
||
</span></span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Injectable</span>, <span style="color:#a6e22e">inject</span>, <span style="color:#a6e22e">OnDestroy</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'@angular/core'</span>;
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Subject</span>, <span style="color:#a6e22e">Observable</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'rxjs'</span>;
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">environment</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../environments/environment'</span>;
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">SpaceStorageService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/services/space-storage.service'</span>;
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">AuthService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/auth/services/auth.service'</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">WSEvent</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartId?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userId?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">payload?</span>: <span style="color:#66d9ef">any</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">timestamp?</span>: <span style="color:#66d9ef">string</span>;
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@Injectable</span>({ <span style="color:#a6e22e">providedIn</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'root'</span> })
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttWSService</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnDestroy</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">spaceStorage</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">SpaceStorageService</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">auth</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">AuthService</span>);
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">socket</span>: <span style="color:#66d9ef">WebSocket</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">eventSubject</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Subject</span><<span style="color:#f92672">WSEvent</span>>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">reconnectTimer</span>: <span style="color:#66d9ef">any</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Observable для подписки на события */</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">get</span> <span style="color:#a6e22e">events$</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Observable</span><<span style="color:#f92672">WSEvent</span>> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">asObservable</span>();
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Подключиться к комнате ганта */</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">connect</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">chartId</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">getAccessToken</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>, <span style="color:#a6e22e">token</span>);
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WebSocket</span>(<span style="color:#a6e22e">wsUrl</span>);
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onopen</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Connected to gantt </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||
</span></span><span style="display:flex;"><span> };
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onmessage</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">msg</span>) <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">data</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">next</span>(<span style="color:#a6e22e">event</span>);
|
||
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">'[WS] Parse error'</span>, <span style="color:#a6e22e">e</span>);
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> };
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onclose</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Closed: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span> <span style="color:#f92672">!==</span> <span style="color:#ae81ff">1000</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">scheduleReconnect</span>();
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> };
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onerror</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">close</span>();
|
||
</span></span><span style="display:flex;"><span> };
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Отключиться */</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">disconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">clearTimeout</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">close</span>(<span style="color:#ae81ff">1000</span>, <span style="color:#e6db74">'manual'</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">token</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">base</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">environment</span>.<span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">||</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">protocol</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">'http'</span>, <span style="color:#e6db74">'ws'</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">'//'</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">host</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">base</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/ws/gantt/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">?token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">scheduleReconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">return</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">setTimeout</span>(() <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">'[WS] Reconnecting...'</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||
</span></span><span style="display:flex;"><span> }, <span style="color:#ae81ff">3000</span>);
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h3 id="42-интеграция-в-gantt-chart-canvas-компонент">4.2. Интеграция в gantt-chart-canvas компонент</h3>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// В gantt-chart-canvas.component.ts
|
||
</span></span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttChartCanvasComponent</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnInit</span>, <span style="color:#a6e22e">OnDestroy</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ws</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">GanttWSService</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">onlineUsers</span>: <span style="color:#66d9ef">Set</span><<span style="color:#f92672">string</span>> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Set</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">wsSubscription</span>: <span style="color:#66d9ef">Subscription</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnInit</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Подключиться к WS при загрузке ганта
|
||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">events$</span>.<span style="color:#a6e22e">subscribe</span>(<span style="color:#a6e22e">event</span> <span style="color:#f92672">=></span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>);
|
||
</span></span><span style="display:flex;"><span> });
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">unsubscribe</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">disconnect</span>();
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Если это событие от текущего пользователя — пропускаем
|
||
</span></span></span><span style="display:flex;"><span> <span style="color:#75715e">// (мы уже обновили UI оптимистично или после HTTP-ответа)
|
||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentUserId</span>) <span style="color:#66d9ef">return</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">switch</span> (<span style="color:#a6e22e">event</span>.<span style="color:#66d9ef">type</span>) {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_created'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_updated'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_deleted'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'sections_reordered'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reorderSectionsLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">ordered_ids</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_created'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_updated'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_deleted'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_joined'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_left'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// === Безопасность ===
|
||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'access_revoked'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Доступ к пространству был отозван'</span> }
|
||
</span></span><span style="display:flex;"><span> });
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'chart_archived'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Диаграмма была заархивирована'</span> }
|
||
</span></span><span style="display:flex;"><span> });
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'role_changed'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentRole</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">role</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateEditableState</span>();
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'token_expired'</span><span style="color:#f92672">:</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/auth/login'</span>]);
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">redrawCanvas</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Перерисовать Konva canvas с обновлёнными данными
|
||
</span></span></span><span style="display:flex;"><span> }
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><h3 id="43-индикатор-онлайн-пользователей">4.3. Индикатор онлайн-пользователей</h3>
|
||
<p>Добавить в шаблон canvas-компонента:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e"><!-- Онлайн-индикатор --></span>
|
||
</span></span><span style="display:flex;"><span><<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"online-indicator"</span> <span style="color:#960050;background-color:#1e0010">*</span><span style="color:#a6e22e">ngIf</span><span style="color:#f92672">=</span><span style="color:#e6db74">"onlineUsers.size > 0"</span>>
|
||
</span></span><span style="display:flex;"><span> <<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"pulse-dot"</span>></<span style="color:#f92672">span</span>>
|
||
</span></span><span style="display:flex;"><span> {{ onlineUsers.size }} онлайн
|
||
</span></span><span style="display:flex;"><span></<span style="color:#f92672">div</span>>
|
||
</span></span></code></pre></div><hr>
|
||
<h2 id="5-безопасность">5. Безопасность</h2>
|
||
<h3 id="51-авторизация-при-подключении">5.1. Авторизация при подключении</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Этап</th>
|
||
<th>Проверка</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>WS Handshake</td>
|
||
<td>JWT в query-параметре <code>?token=...</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Подписка на комнату</td>
|
||
<td>Проверка membership в space</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Каждое broadcast-событие</td>
|
||
<td>Hub знает userId каждого клиента</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 id="52-сценарии-отзыва-доступа">5.2. Сценарии отзыва доступа</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Событие</th>
|
||
<th>Триггер</th>
|
||
<th>Действие на клиенте</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>access_revoked</code></td>
|
||
<td>Исключение из space</td>
|
||
<td>Редирект на список гантов</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>chart_archived</code></td>
|
||
<td>Архивация ганта</td>
|
||
<td>Редирект на список гантов</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>role_changed</code></td>
|
||
<td>Смена роли (editor→viewer)</td>
|
||
<td>Отключить редактирование</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>token_expired</code></td>
|
||
<td>Токен протух</td>
|
||
<td>Редирект на логин</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 id="53-защита-от-злоупотреблений">5.3. Защита от злоупотреблений</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Угроза</th>
|
||
<th>Мера</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Спам WS-сообщений</td>
|
||
<td>Rate limit: 30 msg/min на клиента, <code>ReadLimit</code> в gorilla/websocket</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Подключение без прав</td>
|
||
<td>Проверка space membership при handshake</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Истёкший токен</td>
|
||
<td>Ping/pong + периодическая проверка JWT (раз в 5 мин)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>XSS через WS</td>
|
||
<td>Payload — JSON, sanitize имён на frontend</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Мутации через WS</td>
|
||
<td>WS <strong>только для чтения</strong> (уведомления). Мутации — через HTTP API с полной валидацией</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 id="54-nginx-конфигурация">5.4. Nginx конфигурация</h3>
|
||
<p>В конфиг <code>sand.a2v.space</code> добавить:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># WebSocket проксирование
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/api/v1/ws/</span> {
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">"upgrade"</span>;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">86400s</span>; <span style="color:#75715e"># 24h для long-lived WS
|
||
</span></span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#e6db74">86400s</span>;
|
||
</span></span><span style="display:flex;"><span>}
|
||
</span></span></code></pre></div><hr>
|
||
<h2 id="6-оценка-трудозатрат">6. Оценка трудозатрат</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Компонент</th>
|
||
<th>Строки кода</th>
|
||
<th>Время</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>internal/ws/</code> (hub, client, message)</td>
|
||
<td>~200</td>
|
||
<td>2-3 часа</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>ws_handler.go</code> (endpoint)</td>
|
||
<td>~60</td>
|
||
<td>30 мин</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Интеграция broadcast в handlers</td>
|
||
<td>~20 (по строке на endpoint)</td>
|
||
<td>30 мин</td>
|
||
</tr>
|
||
<tr>
|
||
<td>События безопасности в services</td>
|
||
<td>~30</td>
|
||
<td>30 мин</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>gantt-ws.service.ts</code> (Angular)</td>
|
||
<td>~80</td>
|
||
<td>1 час</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Интеграция в canvas-компонент</td>
|
||
<td>~80</td>
|
||
<td>1-2 часа</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Индикатор онлайн</td>
|
||
<td>~30</td>
|
||
<td>30 мин</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Nginx конфигурация</td>
|
||
<td>~10</td>
|
||
<td>10 мин</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Тестирование</td>
|
||
<td>—</td>
|
||
<td>2-3 часа</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Итого</strong></td>
|
||
<td><strong>~510 строк</strong></td>
|
||
<td><strong>~1-2 дня</strong></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr>
|
||
<h2 id="7-порядок-реализации">7. Порядок реализации</h2>
|
||
<ol>
|
||
<li><strong>Backend: <code>internal/ws/</code></strong> — hub, client, message types</li>
|
||
<li><strong>Backend: WS endpoint</strong> — handler + route</li>
|
||
<li><strong>Backend: Broadcast в handlers</strong> — интеграция в существующие CRUD-эндпоинты</li>
|
||
<li><strong>Backend: События безопасности</strong> — access_revoked, chart_archived, role_changed</li>
|
||
<li><strong>Nginx: WS location</strong> — проксирование WebSocket</li>
|
||
<li><strong>Frontend: WS service</strong> — подключение, реконнект, парсинг событий</li>
|
||
<li><strong>Frontend: Интеграция в canvas</strong> — обработка событий, обновление Konva</li>
|
||
<li><strong>Frontend: Индикатор онлайн</strong> — визуальная обратная связь</li>
|
||
<li><strong>Тестирование</strong> — многопользовательский сценарий, отзыв прав, обрывы связи</li>
|
||
</ol>
|
||
<hr>
|
||
<h2 id="8-дальнейшие-улучшения-после-mvp">8. Дальнейшие улучшения (после MVP)</h2>
|
||
<ul>
|
||
<li><strong>Cursor других пользователей</strong> — показывать позицию курсора на таймлайне</li>
|
||
<li><strong>Блокировка элемента</strong> — «кто-то редактирует эту задачу» (optimistic locking через WS)</li>
|
||
<li><strong>Чат в ганте</strong> — комментарии к задачам прямо на канвасе</li>
|
||
<li><strong>History/undo</strong> — откат изменений с визуальной историей</li>
|
||
<li><strong>Уведомления</strong> — push-уведомления при @mention в комментариях к задачам</li>
|
||
</ul>
|
||
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="site-footer">
|
||
<div class="container">
|
||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||
</div>
|
||
</footer>
|
||
|
||
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||
|
||
|
||
<script src="../../js/search.js"></script>
|
||
<script src="../../js/creds.js"></script>
|
||
|
||
|
||
</body>
|
||
</html>
|