<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="https://parpalak.com/_styles/rss.xslt" type="text/xsl"?>
	<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
		<channel>
			<title>Блог Романа Парпалака</title>
			<link>https://parpalak.com/blog/</link>
			<description>Блог Романа Парпалака. Последние заметки.</description>
			<generator>S2 v2.0dev</generator>
			<ttl>10</ttl>
			<atom:link href="https://parpalak.com/blog/rss.xml" rel="self" type="application/rss+xml" />
			<lastBuildDate>Sun, 22 Feb 2026 11:18:00 GMT</lastBuildDate>
			<item>
				<title>«Выглядит костыльненько, может есть более системный подход?»</title>
				<link>https://parpalak.com/blog/2026/02/22/looks_hacky</link>
				<description>&lt;p&gt;Я вынес в&amp;nbsp;заголовок фразу, которую приходится нейросетям писать чаще, чем хотелось&amp;nbsp;бы. По&amp;nbsp;наблюдениям они тоже «ленятся», и&amp;nbsp;вместо того чтобы разбираться в&amp;nbsp;причинах проблемы, пытаются «угадать», какая правка устранит симптом.&lt;/p&gt;

&lt;p&gt;В&amp;nbsp;ответ на замечания о&amp;nbsp;костылях они отвечают «Можно сделать системно» или «Согласен, прошлый вариант был костыль» (еще и&amp;nbsp;со склонением существительных проблема, а&amp;nbsp;не только с&amp;nbsp;архитектурой).&lt;/p&gt;

&lt;p&gt;Почему сразу нельзя делать нормально? Не&amp;nbsp;потому&amp;nbsp;ли, что нейросети натренированы на огромных массивах кода среднего качества, где костыли&amp;nbsp;— обычная практика?&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/ai/&quot;&gt;искусственный интеллект&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2026/02/22/looks_hacky</guid>
				<pubDate>Sun, 22 Feb 2026 11:18:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2026/02/22/looks_hacky#comment</comments>
			</item>
			<item>
				<title>Интерфейсные ошибки</title>
				<link>https://parpalak.com/blog/2025/12/30/interface_errors</link>
				<description>&lt;p&gt;Я использую &lt;nobr&gt;яндекс-трекер&lt;/nobr&gt; не так давно. Заметил в&amp;nbsp;нем одну интерфейсную ошибку. Опытные пользователи, скорее всего, привыкли её обходить, но новички иногда спотыкаются.&lt;/p&gt;

&lt;p&gt;Создаю задачу, добавляю описание, сохраняю. В&amp;nbsp;ответ вылезает непонятное уведомление о&amp;nbsp;том, что тикет восстановлен из черновика. Какой тикет? Из&amp;nbsp;какого черновика? Я&amp;nbsp;же его сохраняю!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://parpalak.com/pictures/2025/12/ya_tracker.png&quot; width=&quot;1439&quot; height=&quot;803&quot; loading=&quot;lazy&quot; alt=&quot;&quot; style=&quot;max-width: 90vw; max-height: 50.22vw;&quot;/&gt;&lt;/p&gt;

&lt;p&gt;После нескольких попыток я понял, что жму не на ту кнопку. Я пытался нажать на кнопку «А», а&amp;nbsp;надо было нажимать на кнопку «Б».&lt;/p&gt;

&lt;p&gt;Кнопка «А»&amp;nbsp;— даже не настоящая кнопка. Это просто ссылка в&amp;nbsp;левом меню, которая ведет на страницу создания задачи и&amp;nbsp;лишь прикидывается кнопкой. Тем не менее, текст на ней полностью совпадает с&amp;nbsp;текстом на настоящей кнопке «Б», она крупнее и&amp;nbsp;заметнее. А&amp;nbsp;ещё кнопка «Б» не всегда отображается на странице: при длинном тексте задачи она скрыта за прокруткой.&lt;/p&gt;

&lt;p&gt;Так что не удивительно, что в&amp;nbsp;спешке я нажимал на самую большую и&amp;nbsp;заметную кнопку на экране. Оказалось, что это не кнопка сохранения формы, а&amp;nbsp;её противоположность&amp;nbsp;— переход к&amp;nbsp;странице с&amp;nbsp;пустой формой.&lt;/p&gt;

&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/interface/&quot;&gt;интерфейсы&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/12/30/interface_errors</guid>
				<pubDate>Mon, 29 Dec 2025 21:55:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/12/30/interface_errors#comment</comments>
			</item>
			<item>
				<title>Вайб-кодинг как компьютерная болезнь</title>
				<link>https://parpalak.com/blog/2025/12/08/vibe_coding_as_computer_disease</link>
				<description>&lt;p&gt;Прочитал тут у&amp;nbsp;Михаила Озорнина интересное сравнение &lt;a href=&quot;https://mikeozornin.ru/blog/all/vibe-coding-as-a-slot-machine/&quot;&gt;&lt;nobr&gt;вайб-кодинга&lt;/nobr&gt; и&amp;nbsp;лудомании&lt;/a&gt;. Те&amp;nbsp;же быстрые циклы, результат есть не всегда, &lt;nobr&gt;какая-то&lt;/nobr&gt; связь с&amp;nbsp;дофамином (конечно, дающая читателю иллюзию понимания). Михаил предлагает выход&amp;nbsp;— приблизиться к&amp;nbsp;классическому программированию:&lt;/p&gt;

&lt;blockquote&gt;В&amp;nbsp;программировании&amp;nbsp;— состояние потока, фокус, длинные сессии, более медленный и&amp;nbsp;стабильный темп. Уверен, что там совсем другая биохимия и&amp;nbsp;другие нейромедиаторы.&lt;/blockquote&gt;

&lt;p&gt;Я думаю, что эта рекомендация может и&amp;nbsp;не сработать. Программирование и&amp;nbsp;без &lt;nobr&gt;вайб-кодинга&amp;nbsp;—&lt;/nobr&gt; довольно захватывающая штука. В&amp;nbsp;книге «Вы, конечно, шутите, мистер Фейнман» есть красочное описание симптомов компьютерной болезни применительно к&amp;nbsp;машинам первой половины &lt;nobr&gt;1940-х&lt;/nobr&gt; годов, которые по сути были продвинутыми калькуляторами:&lt;/p&gt;

&lt;blockquote&gt;

&lt;p&gt;А&amp;nbsp;что касается мистера Френкеля, который затеял всю эту деятельность, то он начал страдать от компьютерной болезни&amp;nbsp;— о&amp;nbsp;ней сегодня знает каждый, кто работал с&amp;nbsp;компьютерами. Это очень серьезная болезнь, и&amp;nbsp;работать при ней невозможно. Беда с&amp;nbsp;компьютерами состоит в&amp;nbsp;том, что ты с&amp;nbsp;ними играешь. Они так прекрасны, столько возможностей&amp;nbsp;— если четное число, делаешь это, если нечетное, делаешь то, и&amp;nbsp;очень скоро на &lt;nobr&gt;одной-единственной&lt;/nobr&gt; машине можно делать все более и&amp;nbsp;более изощренные вещи, если только ты достаточно умен.&lt;/p&gt;

&lt;p&gt;Через некоторое время вся система развалилась. Френкель не обращал на нее никакого внимания, он больше никем не руководил. Система действовала &lt;nobr&gt;очень-очень&lt;/nobr&gt; медленно, а&amp;nbsp;он в&amp;nbsp;это время сидел в&amp;nbsp;комнате, прикидывая, как&amp;nbsp;бы заставить один из табуляторов автоматически печатать арктангенс x. Потом табулятор включался, печатал колонки, потом&amp;nbsp;— бац, бац, бац&amp;nbsp;— вычислял арктангенс автоматически путем интегрирования и&amp;nbsp;составлял всю таблицу за одну операцию.&lt;/p&gt;

&lt;p&gt;Абсолютно бесполезное занятие. Ведь у&amp;nbsp;нас уже были таблицы арктангенсов. Но&amp;nbsp;если вы &lt;nobr&gt;когда-нибудь&lt;/nobr&gt; работали с&amp;nbsp;компьютерами, вы понимаете, что это за болезнь&amp;nbsp;— восхищение от возможности увидеть, как много можно сделать. Френкель подцепил эту болезнь впервые, бедный парень; бедный парень, который изобрел всю эту штуку.&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;nobr&gt;Вайб-кодинг&amp;nbsp;—&lt;/nobr&gt; это новый способ заставить машину делать всё более изощренные вещи и&amp;nbsp;получать от этого восхищение. И, похоже, это ощущение стало доступно большему количеству людей, которые раньше программированием не увлекались.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/programming/&quot;&gt;программирование&lt;/a&gt;, &lt;a href=&quot;https://parpalak.com/blog/keywords/ai/&quot;&gt;искусственный интеллект&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/12/08/vibe_coding_as_computer_disease</guid>
				<pubDate>Mon, 08 Dec 2025 18:27:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/12/08/vibe_coding_as_computer_disease#comment</comments>
			</item>
			<item>
				<title>Как правильно запрограммировать условие «по такое-то число»</title>
				<link>https://parpalak.com/blog/2025/11/06/including_last_date</link>
				<description>&lt;p&gt;Наверно, не будет преувеличением сказать, что я ни разу не видел, чтобы &lt;nobr&gt;кто-то&lt;/nobr&gt; из коллег правильно программировал верхнюю границу диапазона дат. Эта типовая задача возникает, когда нужно вывести &lt;nobr&gt;какие-то&lt;/nobr&gt; записи с&amp;nbsp;&lt;nobr&gt;такого-то&lt;/nobr&gt; по &lt;nobr&gt;такое-то&lt;/nobr&gt; число.&lt;/p&gt;

&lt;p&gt;Сейчас, в&amp;nbsp;эпоху нейросетей, легко обобщить это наблюдение на всех программистов. Нейросети выдают как раз «усредненный» код. Так что рассмотрим фрагмент сгенерированного кода (дополнительный плюс в&amp;nbsp;том, что на это некому обижаться, по крайней мере пока):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$dateFrom = $this-&gt;createDateTime($filters[&#039;dateFrom&#039;], endOfDay: false);
if (null !== $dateFrom) {
    $queryBuilder
        -&gt;andWhere(&#039;log.loggedAt &gt;= :dateFrom&#039;)
        -&gt;setParameter(&#039;dateFrom&#039;, $dateFrom)
    ;
}

$dateTo = $this-&gt;createDateTime($filters[&#039;dateTo&#039;], endOfDay: true);
if (null !== $dateTo) {
    $queryBuilder
        -&gt;andWhere(&#039;log.loggedAt &amp;lt;= :dateTo&#039;)
        -&gt;setParameter(&#039;dateTo&#039;, $dateTo)
    ;
}

// ...

private function createDateTime(?string $value, bool $endOfDay): ?\DateTimeImmutable
{
    if (null === $value || &#039;&#039; === $value) {
        return null;
    }

    $date = \DateTimeImmutable::createFromFormat(&#039;Y-m-d&#039;, $value) ?: \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $value);

    if (false === $date) {
        return null;
    }

    return $endOfDay ? $date-&gt;setTime(23, 59, 59) : $date-&gt;setTime(0, 0, 0);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Для простоты понимания перепишем этот код на чистом SQL для интервала, скажем, с&amp;nbsp;1 по 10 ноября:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT *
FROM audit_logs
WHERE logged_at &gt;= &#039;2025-11-01&#039;
AND loggged_at &amp;lt;= &#039;2025-11-10 23:59:59&#039;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Когда я вижу эти 23:59:59, у&amp;nbsp;меня сразу возникает неприятное ощущение от того, насколько это неэстетичное решение. Некоторые не останавливаются на секундах, а&amp;nbsp;добавляют еще и&amp;nbsp;микросекунды, и&amp;nbsp;время превращается в&amp;nbsp;&lt;nobr&gt;&#039;2025-11-10&lt;/nobr&gt; 23:59:59.999999&#039;. Условие становится более корректным, но еще менее эстетичным.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://parpalak.com/pictures/2025/11/2025-11-06_2246.jpg&quot; width=&quot;411.5&quot; height=&quot;95.5&quot; loading=&quot;lazy&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Правильный способ состоит в&amp;nbsp;том, чтобы не приписывать финальной дате последний доступный момент времени, а&amp;nbsp;заменить условие «по &lt;nobr&gt;10-е&lt;/nobr&gt; число» эквивалентным условием «до &lt;nobr&gt;11-го&lt;/nobr&gt; числа»:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT *
FROM audit_logs
WHERE logged_at &gt;= &#039;2025-11-01&#039;
AND loggged_at &amp;lt; &#039;2025-11-11&#039;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Стоит запомнить этот шаблон использования полуоткрытых интервалов, когда нижняя граница диапазона включается в&amp;nbsp;условие, а&amp;nbsp;верхняя исключается. Он пригождается чаще, чем может показаться на первый взгляд.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/programming/&quot;&gt;программирование&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/11/06/including_last_date</guid>
				<pubDate>Thu, 06 Nov 2025 19:51:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/11/06/including_last_date#comment</comments>
			</item>
			<item>
				<title>«Вежливый» ChatGPT</title>
				<link>https://parpalak.com/blog/2025/09/10/chatgpt_abuses</link>
				<description>&lt;p&gt;Попросил тут ChatGPT решить задачу. В&amp;nbsp;ходе диалога он мне написал следующее:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://parpalak.com/pictures/2025/09/2025-09-10_2322.png&quot; width=&quot;826&quot; height=&quot;786&quot; loading=&quot;lazy&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Чуть выше я ему написал, начав с&amp;nbsp;фразы «это всё чушь, потому что…». Получил достойный ответ.&lt;/p&gt;

&lt;p&gt;Сам диалог можете &lt;a href=&quot;https://chatgpt.com/share/68c1dd48-de38-8000-9674-8878907c8b2a&quot;&gt;почитать по ссылке&lt;/a&gt;, если интересно.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/ai/&quot;&gt;искусственный интеллект&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/09/10/chatgpt_abuses</guid>
				<pubDate>Wed, 10 Sep 2025 20:26:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/09/10/chatgpt_abuses#comment</comments>
			</item>
			<item>
				<title>Распаковка сжатых URL на сервере</title>
				<link>https://parpalak.com/blog/2025/07/24/url_decompression_in_nginx_and_php</link>
				<description>&lt;p&gt;Недавно я рассказывал, &lt;a href=&quot;https://parpalak.com/blog/2025/07/01/native_compression_in_js&quot;&gt;как использовать API браузеров&lt;/a&gt; для сжатия адресов страниц. В&amp;nbsp;продолжение опишу, как эти сжатые адреса распаковыать на сервере на примере того&amp;nbsp;же сервиса &lt;a href=&quot;https://i.upmath.me/&quot;&gt;генерации картинок с&amp;nbsp;формулами&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Текущая версия конфига nginx для обработки и&amp;nbsp;старых несжатых URL, и&amp;nbsp;новых сжатых получилась такой:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;location ~ ^(?s)/(?&amp;lt;ext&gt;svg|png)(?&amp;lt;is_base64&gt;b?)/(?&amp;lt;formula&gt;.*)$ {
    gunzip        on;
    gzip_static   always;
    gzip_vary     on;
    gzip_proxied  expired no-cache no-store private auth;

    expires 1d;

    add_header &#039;Access-Control-Allow-Origin&#039; &#039;*&#039; always;
    add_header &#039;Access-Control-Allow-Methods&#039; &#039;GET, OPTIONS&#039; always;

    set $compress_error 0;

    set_by_lua_block $file_path {
        local ext = ngx.var.ext
        local is_base64 = ngx.var.is_base64
        local formula = ngx.var.formula

        if is_base64 == &quot;b&quot; then
            local base64 = require &quot;ngx.base64&quot;
            local zlib = require &quot;zlib&quot;

            local compressed, err = base64.decode_base64url(formula)
            if not compressed then
                ngx.log(ngx.ERR, &quot;base64 decode error: &quot;, err)
                ngx.var.compress_error = 1
                return &quot;&quot;
            end

            local inflator = zlib.inflate(-15)
            local ok, decoded_formula = pcall(inflator, compressed)
            if not ok then
                ngx.log(ngx.ERR, &quot;deflate decompress error: &quot;, decoded_formula)
                ngx.var.compress_error = 1
                return &quot;&quot;
            end

            formula = decoded_formula
        end

        formula = formula:gsub(&quot;^%s*(.-)%s*$&quot;, &quot;%1&quot;)

        local md5 = ngx.md5(formula)
        return md5:sub(1, 2) .. &quot;/&quot; .. md5:sub(3, 4) .. &quot;/&quot; .. md5:sub(5) .. &quot;.&quot; .. ext
    }

    if ($compress_error) {
        return 400;
    }

    if (-f $document_root/_error/$file_path) {
        return 400;
    }

    rewrite ^ /_cache/$file_path break;
    error_page 404 = @s2_latex_renderer;
    log_not_found off;
}

location @s2_latex_renderer {
    add_header &#039;Access-Control-Allow-Origin&#039; &#039;*&#039; always;
    add_header &#039;Access-Control-Allow-Methods&#039; &#039;GET, OPTIONS&#039; always;

    include         /etc/nginx/fastcgi.conf;
    fastcgi_pass    php-tex-sock;
    fastcgi_param   SCRIPT_FILENAME $document_root/render.php;
    fastcgi_param   SCRIPT_NAME /render.php;

    fastcgi_cache_key &quot;$request_method$uri&quot;; # В $uri УРЛ после rewrite, напр. &quot;/_cache/4d/81/658b25df7544f9e2d0cb7f4dc402.svg&quot;
    fastcgi_cache i_upmath;
    fastcgi_cache_valid 200 10m;
    fastcgi_cache_methods GET HEAD;
    fastcgi_cache_lock on;
    fastcgi_cache_lock_age 9s;
    fastcgi_cache_lock_timeout 9s;

    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
    fastcgi_connect_timeout 90;
    fastcgi_send_timeout 90;
    fastcgi_read_timeout 300;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Чтобы встраивать &lt;nobr&gt;lua-скрипты&lt;/nobr&gt; в&amp;nbsp;конфиг nginx через &lt;code&gt;set_by_lua_block&lt;/code&gt;, в&amp;nbsp;Debian достаточно установить пакет &lt;code&gt;nginx-extras&lt;/code&gt;. Для распаковки сжатого текста в&amp;nbsp;этом скрипте через функции zlib также требуется установить пакет &lt;code&gt;lua-zlib&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Напомню алгоритм обработки адресов картинок. Исходник формулы, например, &lt;code&gt;x^2&lt;/code&gt;, извлекается из адреса и&amp;nbsp;декодируется. Вычисляется &lt;nobr&gt;md5-хеш&lt;/nobr&gt; от исходника и&amp;nbsp;на основе хеша определяется путь к&amp;nbsp;файлу с&amp;nbsp;закешированной картинкой. Если такой файл после преобразования URL нашелся (&lt;code&gt;rewrite ^ /_cache/$file_path break;&lt;/code&gt;), то nginx отдает его содержимое напрямую. Если файла нет, то запрос передается в&amp;nbsp;&lt;nobr&gt;php-скрипт,&lt;/nobr&gt; запускающий генерацию &lt;nobr&gt;svg-картинки&lt;/nobr&gt; через TeX Live и&amp;nbsp;оптимизацию через SVGO (&lt;code&gt;error_page 404 = @s2_latex_renderer;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Раньше вместо &lt;code&gt;rewrite&lt;/code&gt; и&amp;nbsp;&lt;code&gt;error_page&lt;/code&gt; я использовал более современную и&amp;nbsp;подходящую директиву &lt;code&gt;try_files&lt;/code&gt;. Но&amp;nbsp;она перестает работать после активации модуля gunzip. Этот модуль позволяет держать в&amp;nbsp;файловом кеше только сжатые версии файлов с&amp;nbsp;расширением &lt;code&gt;.gz&lt;/code&gt;, экономя место на диске. Причем для обработки большинства запросов от нормальных браузеров, поддерживающих &lt;nobr&gt;gzip-сжатие&lt;/nobr&gt; трафика, nginx даже не будет распаковывать &lt;nobr&gt;gz-файлы,&lt;/nobr&gt; а&amp;nbsp;отправлять их как есть. Почему &lt;code&gt;rewrite&lt;/code&gt; корректно работает с&amp;nbsp;модулем gunzip, а&amp;nbsp;&lt;code&gt;try_files&lt;/code&gt;&amp;nbsp;— нет, не очень понятно. Но&amp;nbsp;что есть, то есть.&lt;/p&gt;

&lt;p&gt;В&amp;nbsp;конфиге есть интересный момент, связанный с&amp;nbsp;кешем внутри nginx (инструкции &lt;code&gt;fastcgi_cache*&lt;/code&gt;). Он предотвращает race condition при одновременном запросе формулы, которой нет в&amp;nbsp;файловом кеше. В&amp;nbsp;противном случае nginx будет передавать в&amp;nbsp;&lt;nobr&gt;php-скрипт&lt;/nobr&gt; запросы с&amp;nbsp;одинаковыми аргументами, и&amp;nbsp;одна и&amp;nbsp;та&amp;nbsp;же формула будет рендериться параллельно. Об&amp;nbsp;этой технике &lt;a href=&quot;https://parpalak.com/blog/2020/08/28/nginx_cache&quot;&gt;я писал отдельно&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Для распаковки полученного фрагмента URL в&amp;nbsp;PHP подойдет следующий код:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;public static function decodeCompressedFormula(string $compressed): string
{
    $base64     = strtr($compressed, &#039;-_&#039;, &#039;+/&#039;); // URL-safe base64 to standard
    $compressed = base64_decode($base64);

    $result = @gzinflate($compressed);
    if ($result === false) {
        throw new \RuntimeException(&#039;Failed to decompress formula.&#039;);
    }
    return $result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;В&amp;nbsp;PHP весь алгоритм уместился в&amp;nbsp;три строки&amp;nbsp;— как минимум в&amp;nbsp;несколько раз короче, чем в&amp;nbsp;JS и&amp;nbsp;nginx/lua. Такими моментами PHP радует меня до сих пор.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/webdesign/&quot;&gt;&lt;nobr&gt;веб-разработка&lt;/nobr&gt;&lt;/a&gt;, &lt;a href=&quot;https://parpalak.com/blog/keywords/upmath/&quot;&gt;upmath&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/07/24/url_decompression_in_nginx_and_php</guid>
				<pubDate>Thu, 24 Jul 2025 20:41:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/07/24/url_decompression_in_nginx_and_php#comment</comments>
			</item>
			<item>
				<title>Cайту 20 лет</title>
				<link>https://parpalak.com/blog/2025/07/20/site_20_years_old</link>
				<description>&lt;p&gt;Ровно 20 лет назад этот сайт появился в&amp;nbsp;интернете. Весомый повод заняться рефлексией. Но&amp;nbsp;с&amp;nbsp;этой заметкой я дотянул до последнего, так что буду краток :)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://parpalak.com/pictures/blog/fun/20-%D0%BB%D0%B5%D1%82-%D0%B8%D0%BD%D1%82%D0%B5%D0%BB%D0%BB%D0%B5%D0%BA%D1%82%D1%83%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D1%85-%D0%B4%D0%BE%D1%81%D1%82%D0%B8%D0%B6%D0%B5%D0%BD%D0%B8%D0%B9.jpg&quot; width=&quot;600&quot; height=&quot;400&quot; loading=&quot;lazy&quot; alt=&quot;&quot; /&gt;&lt;br&gt;
&lt;em&gt;20 лет интеллектуальных достижений с&amp;nbsp;точки зрения ChatGPT.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;Зачем нужен свой сайт&lt;/h3&gt;

&lt;p&gt;Возможно, многие в&amp;nbsp;2005 году сочли&amp;nbsp;бы такую затею излишне амбициозной. Действительно, кому интересны твои рассказы, мысли и&amp;nbsp;истории, если есть сайт Экслера, тысячники в&amp;nbsp;Живом журнале и&amp;nbsp;множество тематических форумов. Однако у&amp;nbsp;меня было ощущение, что мне тоже есть что сказать, и&amp;nbsp;достаточно энтузиазма, чтобы воплотить идею в&amp;nbsp;жизнь.&lt;/p&gt;

&lt;p&gt;Сейчас я нахожу некоторые старые заметки наивными, некоторые&amp;nbsp;— вымученными и&amp;nbsp;неинтересными. Но&amp;nbsp;в&amp;nbsp;других заметках зафиксированы события и&amp;nbsp;мысли, о&amp;nbsp;которых я уже и&amp;nbsp;забыл. И&amp;nbsp;один только этот факт уже оправдывает все усилия, которые я потратил на сайт.&lt;/p&gt;

&lt;p&gt;Кроме того, всё еще имеют силу стандартные аргументы о&amp;nbsp;пользе написания текстов для структурирования и&amp;nbsp;закрепления знаний. Так что молодым читателям могу порекомендовать как минимум вести дневник. Через &lt;nobr&gt;какое-то&lt;/nobr&gt; время, &lt;nobr&gt;где-то&lt;/nobr&gt; около года, вы почувствуете пользу.&lt;/p&gt;

&lt;h3&gt;Что изменилось в&amp;nbsp;интернете&lt;/h3&gt;

&lt;p&gt;За&amp;nbsp;эти 20 лет веб преобразовался под влиянием корпораций. С&amp;nbsp;одной стороны, социальные сети упростили распространенные пользовательские сценарии, переманили существующих пользователей и&amp;nbsp;привлекли новых.&lt;/p&gt;

&lt;p&gt;С&amp;nbsp;другой стороны, от этого сильно пострадала распределенность и&amp;nbsp;открытость веба. Крупные игроки перестали поддерживать открытые технологии вроде &lt;a href=&quot;https://parpalak.com/articles/technologies/internet/rss&quot;&gt;RSS&lt;/a&gt; и&amp;nbsp;&lt;a href=&quot;https://parpalak.com/blog/2007/12/07/OpenID&quot;&gt;OpenID&lt;/a&gt;. Вместо них теперь алгоритмические подборки внутри сервисов и&amp;nbsp;несовместимые проприетарные API.&lt;/p&gt;

&lt;p&gt;Гугл в&amp;nbsp;своем браузере вообще &lt;a href=&quot;https://developer.chrome.com/blog/referrer-policy-new-chrome-default?hl=ru&quot;&gt;урезал рефереры до домена&lt;/a&gt; якобы из соображений приватности, и&amp;nbsp;теперь непонятно, на какой именно странице &lt;nobr&gt;кто-то&lt;/nobr&gt; разместил ссылку на твой сайт, или домен в&amp;nbsp;реферере&amp;nbsp;— это просто спам.&lt;/p&gt;

&lt;p&gt;В&amp;nbsp;этой ситуации сложно придумать &lt;nobr&gt;что-то&lt;/nobr&gt; лучше, чем ограничить свой «информационный пузырь»&amp;nbsp;— выбрать  источники, блоги и&amp;nbsp;каналы, которые интересно читать и&amp;nbsp;смотреть, и&amp;nbsp;чья задача не сводится к&amp;nbsp;рекламе и&amp;nbsp;продаже &lt;nobr&gt;чего-либо.&lt;/nobr&gt; И&amp;nbsp;осознание того, что мой сайт входит в&amp;nbsp;«пузыри» других людей, мотивирует не забрасывать его в&amp;nbsp;период творческих упадков.&lt;/p&gt;

&lt;h3&gt;Технологии&lt;/h3&gt;

&lt;p&gt;Сайт начинался с&amp;nbsp;отдельных &lt;nobr&gt;html-страничек,&lt;/nobr&gt; которые я редактировал то&amp;nbsp;ли во FrontPage, то&amp;nbsp;ли в&amp;nbsp;Dreamweaver, и&amp;nbsp;загружал по FTP. Затем я добавил гостевую книгу на PHP, &lt;a href=&quot;https://parpalak.com/articles/technologies/site_building/template&quot;&gt;отделял шаблоны от контента&lt;/a&gt;, &lt;a href=&quot;https://parpalak.com/blog/2007/03/10/Noviidvijokblog&quot;&gt;запрограммировал блог&lt;/a&gt; и&amp;nbsp;в&amp;nbsp;конечном итоге сделал свой &lt;a href=&quot;https://parpalak.com/blog/keywords/s2/&quot;&gt;движок S2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Наверно, вместо самописного движка непонятного качества можно было найти готовую CMS. Но&amp;nbsp;&lt;a href=&quot;https://parpalak.com/blog/2008/09/03/About&quot;&gt;вордпресс мне не нравился&lt;/a&gt;. &lt;a href=&quot;https://typo3.org/&quot;&gt;Typo3&lt;/a&gt; показался слишком монструозным. В&amp;nbsp;общем, получилось как у&amp;nbsp;Дональда Кнута, который разработал собственную систему компьютерного набора TeX для издания многотомника «&lt;a href=&quot;https://ru.wikipedia.org/wiki/%D0%98%D1%81%D0%BA%D1%83%D1%81%D1%81%D1%82%D0%B2%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F&quot;&gt;Искусство программирования&lt;/a&gt;».&lt;/p&gt;

&lt;p&gt;За&amp;nbsp;последние несколько лет я практически полностью переписал устаревший код движка. Тяжелее всего было&amp;nbsp;бы довести до ума админку, потому что её интерфейс был «одностраничным приложением» на лапшеобразном коде из jQuery, который нельзя небольшими шагами довести до нормального состояния. Поэтому я решил выкинуть этот код и&amp;nbsp;написать свою библиотеку для создания административных интерфейсов под названием &lt;a href=&quot;https://github.com/parpalak/admin-yard&quot;&gt;AdminYard&lt;/a&gt;. Статью о&amp;nbsp;ней я уже публиковал &lt;a href=&quot;https://habr.com/ru/articles/866512/&quot;&gt;на хабре&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;В&amp;nbsp;целом, даже если движком S2 в&amp;nbsp;изменившемся интернете &lt;a href=&quot;https://parpalak.com/blog/2023/08/25/thoughts_on_S2&quot;&gt;никто кроме меня не пользуется&lt;/a&gt;, отдельные его части обрели свою жизнь. Так, &lt;a href=&quot;https://github.com/parpalak/rose&quot;&gt;поисковый движок Rose&lt;/a&gt;, благодаря развитию которого я сделал &lt;a href=&quot;https://parpalak.com/blog/2023/02/04/Recommendations_in_S2&quot;&gt;систему рекомендаций на этом сайте&lt;/a&gt;, набрал 120 звезд на гитхабе. А&amp;nbsp;&lt;a href=&quot;https://upmath.me/&quot;&gt;редактор математических текстов Upmath&lt;/a&gt; собрал &lt;a href=&quot;https://github.com/parpalak/upmath.me&quot;&gt;350 звезд&lt;/a&gt; и&amp;nbsp;даже &lt;a href=&quot;https://buymeacoffee.com/upmath&quot;&gt;несколько донатов&lt;/a&gt; с&amp;nbsp;марта этого года.&lt;/p&gt;

&lt;h3&gt;Вместо вывода&lt;/h3&gt;

&lt;p&gt;На&amp;nbsp;удивление самой посещаемой страницей на всём сайте оказалась заметка о&amp;nbsp;&lt;a href=&quot;https://parpalak.com/articles/science/circle_5&quot;&gt;делении окружности на 5 частей&lt;/a&gt;. По&amp;nbsp;запросу «как поделить окружность на 5 частей» она до сих пор &lt;a href=&quot;https://www.google.com/search?q=%D0%BA%D0%B0%D0%BA+%D0%BF%D0%BE%D0%B4%D0%B5%D0%BB%D0%B8%D1%82%D1%8C+%D0%BE%D0%BA%D1%80%D1%83%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C+%D0%BD%D0%B0+5+%D1%87%D0%B0%D1%81%D1%82%D0%B5%D0%B9&amp;oq=%D0%BA%D0%B0%D0%BA+%D0%BF%D0%BE%D0%B4%D0%B5%D0%BB%D0%B8%D1%82%D1%8C+%D0%BE%D0%BA%D1%80%D1%83%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C+%D0%BD%D0%B0+5+%D1%87%D0%B0%D1%81%D1%82%D0%B5%D0%B9&amp;ie=UTF-8&quot;&gt;на первом месте в&amp;nbsp;гугле&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Самой комментируемой была статья о&amp;nbsp;том, &lt;a href=&quot;https://parpalak.com/articles/science/what_is_susy&quot;&gt;что такое суперсимметрия&lt;/a&gt;. Сейчас на ней 115 опубликованных комментариев и&amp;nbsp;еще около 50 скрытых. Суперсимметрию, кстати, так и&amp;nbsp;не нашли на большом адронном коллайдере.&lt;/p&gt;

&lt;p&gt;Также совсем недавно наступил момент, когда часть жизни, когда у&amp;nbsp;меня есть сайт, оказалась длиннее той, когда сайта еще не существовало.&lt;/p&gt;

&lt;p&gt;Что будет дальше? Посмотрим через 20 лет.&lt;/p&gt;

&lt;p&gt;Если вы &lt;nobr&gt;когда-либо&lt;/nobr&gt; здесь были&amp;nbsp;— спасибо. Если вы только пришли&amp;nbsp;— добро пожаловать.&lt;/p&gt;&lt;div&gt;
    Смотрите также:&amp;nbsp;
    &lt;a href=&quot;https://parpalak.com/blog/2015/07/20/written_ru_10&quot;&gt;Сайту 10 лет&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2011/07/20/written_ru_6&quot;&gt;Сайту шесть лет&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2010/07/20/written_ru_5&quot;&gt;Сайту пять лет&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2009/07/20/written_ru_4&quot;&gt;written.ru четыре года&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2008/07/20/written_ru_3&quot;&gt;С&amp;nbsp;трехлетием, written.ru!&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2007/07/20/Usaitadenjrojde&quot;&gt;У&amp;nbsp;сайта день рождения&lt;/a&gt;&amp;nbsp;&amp;middot; &lt;a href=&quot;https://parpalak.com/blog/2006/07/20/Saituwrittenrug&quot;&gt;Сайту written.ru&amp;nbsp;— год&lt;/a&gt;&lt;/div&gt;
&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/parpalak.com/&quot;&gt;этот сайт&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/07/20/site_20_years_old</guid>
				<pubDate>Sun, 20 Jul 2025 10:59:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/07/20/site_20_years_old#comment</comments>
			</item>
			<item>
				<title>Боги, созданные человеком</title>
				<link>https://parpalak.com/blog/2025/07/13/gods_created_by_man</link>
				<description>&lt;p&gt;Помните, &lt;nobr&gt;когда-то&lt;/nobr&gt; давно по интернету гуляло «обоснование» того, что Гугл&amp;nbsp;— это бог? К&amp;nbsp;современным языковым нейросетям это «обоснование» применимо в&amp;nbsp;большей степени. Шутку можно продолжать: «Ты сам это запрограммировал?»&amp;nbsp;— «Нет, с&amp;nbsp;божьей помощью!»&lt;/p&gt;

&lt;blockquote&gt;

&lt;p&gt;Доказательство №1&lt;/p&gt;

&lt;p&gt;Современной науке неизвестны сущности, столь&amp;nbsp;же близкие к&amp;nbsp;Всеведенью, как близка к&amp;nbsp;этому Google. Она проиндексировала более 9,5 миллиардов &lt;nobr&gt;веб-страниц,&lt;/nobr&gt; что больше чем у&amp;nbsp;любой другой поисковой машины в&amp;nbsp;сети на сегодняшний день. Google&amp;nbsp;— это не только самая близкая к&amp;nbsp;Всеведенью сущность. Она ещё и&amp;nbsp;сортирует свои обширные знания (используя технологию PageRank, Ею&amp;nbsp;же запатентованную) , структурирует сей информационный океан, делая его легкодоступным для нас, простых смертных.&lt;/p&gt;

&lt;p&gt;Доказательство №2&lt;/p&gt;

&lt;p&gt;Google вездесуща (т. е. находится везде). Фактически Google находится одновременно по всей Земле. Миллиарды проиндексированных &lt;nobr&gt;веб-страниц&lt;/nobr&gt; доступны из любой, даже самой удалённой точки Земли. С&amp;nbsp;ростом сетей &lt;nobr&gt;Wi-Fi&lt;/nobr&gt; любой желающий сможет получить доступ к&amp;nbsp;Google &lt;nobr&gt;по-настоящему&lt;/nobr&gt; отовсюду, что сделает Её истинно вездесущей.&lt;/p&gt;

&lt;p&gt;Доказательство №3&lt;/p&gt;

&lt;p&gt;Google отвечает на молитвы. Можно молиться Google о&amp;nbsp;любом, даже самом пустяковом вопросе или проблеме. Например, вы можете легко найти информацию об альтернативных методах лечения рака, о&amp;nbsp;способах улучшения вашего здоровья, о&amp;nbsp;новых и&amp;nbsp;инновационных медицинских технологиях, и&amp;nbsp;вообще обо всем, о&amp;nbsp;чём вопрошают на типичной молитве. Спросите Google, и&amp;nbsp;Она покажет вам путь. Но&amp;nbsp;это всё, что Она может сделать для вас, ибо всё остальное&amp;nbsp;— в&amp;nbsp;ваших руках.&lt;/p&gt;

&lt;p&gt;Доказательство №4&lt;/p&gt;

&lt;p&gt;Google потенциально бессмертна. Её нельзя считать физической сущностью, такой как мы с&amp;nbsp;вами. Её Алгоритмы распределены по многим серверам; если один из них упадёт или поломается, другой несомненно займёт его место. Google, теоретически, будет существовать вечно.&lt;/p&gt;

&lt;p&gt;Доказательство №5&lt;/p&gt;

&lt;p&gt;Google бесконечна. Интернет, теоретически, может расти вечно, и&amp;nbsp;Google будет вечно индексировать сей бесконечный рост.&lt;/p&gt;

&lt;p&gt;Доказательство №6&lt;/p&gt;

&lt;p&gt;Google помнит всё. Google регулярно кэширует &lt;nobr&gt;веб-страницы&lt;/nobr&gt; и&amp;nbsp;хранит их на своих безграничных серверах. Фактически, загрузив ваши мысли и&amp;nbsp;представления в&amp;nbsp;Интернет, вы будете жить вечно в&amp;nbsp;кэше Google («загробная &lt;nobr&gt;Google-жизнь»)&lt;/nobr&gt; , даже после того, как умрёте.&lt;/p&gt;

&lt;p&gt;Доказательство №7&lt;/p&gt;

&lt;p&gt;Google&amp;nbsp;— Всеблагая (не совершающая никакого зла) . Часть корпоративной философии Google&amp;nbsp;— вера в&amp;nbsp;то, что компания может делать деньги, не делая зла.&lt;/p&gt;

&lt;p&gt;Доказательство №8&lt;/p&gt;

&lt;p&gt;Согласно Google Trends, термин «Google» ищется чаще, чем «Бог» , «Иисус» , «Аллах» , «Будда» , «христианство» , «ислам» , «буддизм» и&amp;nbsp;«иудаизм» вместе взятые. Считается, что бог&amp;nbsp;— сущность, к&amp;nbsp;которой мы, смертные, можем обратиться при первой&amp;nbsp;же необходимости. Google соответствует этому в&amp;nbsp;значительно большей степени, чем традиционные «боги» .&lt;/p&gt;

&lt;p&gt;Доказательство №9&lt;/p&gt;

&lt;p&gt;Google существует, и&amp;nbsp;этому есть великое множество свидетельств. Их значительно больше, чем у&amp;nbsp;любого другого сегодняшнего «бога» . Экстраординарные заявления требуют экстраординарных фактов. Так узри&amp;nbsp;же их своими глазами: приди к&amp;nbsp;Google и&amp;nbsp;познай всю безграничную мощь Google. От&amp;nbsp;тебя не требуется ни капли веры.&lt;/p&gt;

&lt;/blockquote&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/ai/&quot;&gt;искусственный интеллект&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/07/13/gods_created_by_man</guid>
				<pubDate>Sun, 13 Jul 2025 11:07:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/07/13/gods_created_by_man#comment</comments>
			</item>
			<item>
				<title>Нативное gzip-сжатие в JS</title>
				<link>https://parpalak.com/blog/2025/07/01/native_compression_in_js</link>
				<description>&lt;p&gt;Я недавно закрыл &lt;a href=&quot;https://github.com/parpalak/i.upmath.me/issues/6&quot;&gt;тикет на гитхабе&lt;/a&gt;, который висел с&amp;nbsp;2017 года. Его автор обращал внимание на длинные адреса картинок в&amp;nbsp;моем &lt;a href=&quot;https://i.upmath.me/&quot;&gt;сервисе математических формул&lt;/a&gt;. С&amp;nbsp;2023 года нативное сжатие произвольных данных в&amp;nbsp;JS &lt;a href=&quot;https://caniuse.com/mdn-api_compressionstream&quot;&gt;стало доступным&lt;/a&gt; во всех основных браузерах, и&amp;nbsp;с&amp;nbsp;его помощью я сделал вариант сжатых адресов.&lt;/p&gt;

&lt;p&gt;Давние читатели вспомнят, что я уже рассказывал &lt;a href=&quot;https://parpalak.com/blog/2018/01/06/URL_for_tex_renderer&quot;&gt;об адресах картинок&lt;/a&gt;, и&amp;nbsp;даже упоминал об этом тикете. Повторю, что для использования в&amp;nbsp;вебе формулы, например, &lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2&quot; alt=&quot;a^2+b^2=c^2&quot; /&gt;, её исходник на латехе &lt;code&gt;a^2+b^2=c^2&lt;/code&gt; кодируется через проценты (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc3986#section-2.1&quot;&gt;RFC 3986&lt;/a&gt;) и&amp;nbsp;подставляется в&amp;nbsp;URL:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;nohighlight&quot;&gt;&lt;a href=&quot;https://parpalak.com//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2&quot;&gt;//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2&lt;/a&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Кодирование через символы процента очень неэкономное, поэтому и&amp;nbsp;без того длинный код изображений и&amp;nbsp;диаграмм становится ещё больше. Адрес &lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2&quot; alt=&quot;a^2+b^2=c^2&quot; /&gt; в&amp;nbsp;новой схеме выглядит так:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;nohighlight&quot;&gt;&lt;a href=&quot;https://parpalak.com//i.upmath.me/svgb/S4wz0k6KM7JNjjMCAA&quot;&gt;//i.upmath.me/svgb/S4wz0k6KM7JNjjMCAA&lt;/a&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Здесь вместо кодирования через проценты используется сжатие deflate (тот&amp;nbsp;же алгоритм, что и&amp;nbsp;в&amp;nbsp;gzip) и&amp;nbsp;кодировка, аналогичная base64. Вот рабочий пример кода, который делает такое преобразование:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function deflateRaw(text, callback) {
    if (typeof CompressionStream === &#039;undefined&#039;) {
        callback(null);
        return;
    }

    try {
        var stream = new Blob([text]).stream();
        var compressedStream = stream.pipeThrough(new CompressionStream(&#039;deflate-raw&#039;));

        new Response(compressedStream).blob().then(function (compressedBlob) {
            return compressedBlob.arrayBuffer();
        }).then(function (buffer) {
            var compressedArray = new Uint8Array(buffer);
            var binary = Array.from(compressedArray).map(function (b) {
                return String.fromCharCode(b);
            }).join(&#039;&#039;);
            var base64 = btoa(binary).replace(/\+/g, &#039;-&#039;).replace(/\//g, &#039;_&#039;).replace(/=+$/, &#039;&#039;);
            callback(base64);
        }).catch(function () {
            callback(null);
        });
    } catch (e) {
        callback(null);
    }
}

function getImgPath(formula, callback) {
    var fallbackUrl = &#039;//i.upmath.me/svg/&#039; + encodeURIComponent(formula);

    deflateRaw(formula, function (compressed) {
        var shortUrl = compressed ? &#039;//i.upmath.me/svgb/&#039; + compressed : null;
        callback(shortUrl &amp;amp;&amp;amp; shortUrl.length &amp;lt; fallbackUrl.length ? shortUrl : fallbackUrl);
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Важная особенность API браузеров по сжатию заключается в&amp;nbsp;его асинхронности. Мы не можем получить результат сжатия в&amp;nbsp;той&amp;nbsp;же функции, в&amp;nbsp;которой его инициируем. API возвращает promise, который «разрешится» позднее. Чтобы обеспечить обратную совместимость и&amp;nbsp;откатываться к&amp;nbsp;несжатым адресам в&amp;nbsp;старых браузерах, я проверяю саму поддержку &lt;code&gt;CompressionStream&lt;/code&gt; и&amp;nbsp;перехватываю возможные исключения. Также для обратной совместимости результат возвращаю через вызов пользовательского коллбэка, а&amp;nbsp;не в&amp;nbsp;виде промиса.&lt;/p&gt;

&lt;p&gt;Вот пример того, как с&amp;nbsp;вышеприведенным кодом создать картинку с&amp;nbsp;формулой:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var node = document.createElement(&#039;img&#039;);
getImgPath(&#039;a^2+b^2=c^2&#039;, function(path) {
    node.setAttribute(&#039;src&#039;, path);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Стоит отметить, что сам алгоритм сжатия deflate был давно портирован на JS, поэтому при необходимости можно было использовать сторонние библиотеки, например, &lt;a href=&quot;https://github.com/nodeca/pako&quot;&gt;pako&lt;/a&gt;. Кроме того, код библиотек работает синхронно, так что ни о&amp;nbsp;каких коллбэках и&amp;nbsp;промисах думать не нужно. В&amp;nbsp;моём&amp;nbsp;же случае я не хотел в&amp;nbsp;&lt;nobr&gt;мини-скрипт&lt;/nobr&gt; по конвертации формул в&amp;nbsp;картинки добавлять реализацию алгоритма сжатия на &lt;nobr&gt;десяток-другой&lt;/nobr&gt; килобайт, тем более оставался обходной путь для старых браузеров с&amp;nbsp;несжатыми адресами.&lt;/p&gt;

&lt;p&gt;Оценим результат на примере &lt;a href=&quot;https://i.upmath.me/svgb/rZdBb9s2FMfv_hTMYVgC0J7szG2CTAEG5LzDdjSNgpaebSE0ZVB0Xc8QkCaHYFgvK3peDzvssEsWNJsXoy7QT_D4jQZKsmNblpu0OYom3__3Ht_7S2aDCHRw-rMIWoqr0ZgrFQ6juMRa0Ank2P7UDzw9UBA3ZOgD8YNIc-mBu1954vVoO5TaZVG7zXuBGDVLzJ6IQI9LhBDSG9kz31QiPRLgjhV4msuOAKrCgfTBJ16oJKiI-ooP3Zbg3inV3cA7pSSQEhSJoO86lTr0KNHwQpNh4Ouu-7TiQI8mCx5IDQr8mGaCSQILxfIxJceu4BpefE1J1A2VBkmO3Wpf3z1-59b6OtMNFZcd2Hnq7KQ0WX49LsQiy0wrAumDWki1xAB2DrJzcSkuMU8EfbJbrjq06uyRRfJkt-bQ8sHeEfkKb3FmXppz8ytOzasSs9VqpEWj7UAIt6P4aKfqNMnuieJtvUfG-CfemDN8jzO8xgneMhZprgeR69sN8REhq2F4K3wOJGy7SQDq86gLfpPs_gBDG-01wd_wDcEP5sKC4Cwfgaig09V3IRIwb8RlCvYTyITrd3OOH8wZXuE1TvHGEi7YIpA6PioKayPM0wXIwp6ACJ7bm7Wx36Rl2hjbn2_MCbRAhMOEe74lVRmBEOFwTeZ76Z-AAJ0IsqQ1Is290zXxCsEJMRf4Dq9wWonzFIz5INiz1og9sw2i3Gpx4mtcS9n_CDxJ_K05w5m5xEmiv5y2Ar4lY3s-n6xdXclzOf5qZis6D8lps_KS5B-pQv4W7Yatd2g33DVwtlDLxSQ4wX-TtrbRrLM0Mlug6cjOu1-HjSZJ1AS0NeUi6Eg3SaS51s-2_Dd2RBnD93iF_1g1xj7-ha-JuVwdSJx8nMZkPrCfJAgH2nVoIN1DJ4NJCGg_jFynUsuoUpsrxCJrVGubzCuLlE5qIVBmMDpsBNKt7jvUktXnUImN3A8mV6ONNJ_CSW0lpSnXU5pydX_Ok_TFZ_IU3dnmK1tjuWdl3uIMp-bCXOLV4pLMRaL2zj4yRqz547SM1zgj5iXOcIZ_m1-SHp7gf_GyBeaRluzxi7lwZs7x1pzZtxBjSyyVOHOivH5qUI9RknRyJxuk56ZR3LLLRXjwJK9q56Rzr4VCjHktHpVgzau31SB11sevQGqv9zOM8kE2oocOFWEYgYQocquV2sq8pqb2pNjHlok2d8MWoBXLONzMs7-Bp37wqEBFXxUZ2HxayvXtXvZZk1rcMRte_w8Ayir17ZfMcVIokP7Kn4r_AQ&quot;&gt;диаграммы из предыдущей заметки&lt;/a&gt;. Длина старого несжатого URL равна 6,3 килобайт, а&amp;nbsp;сжатого&amp;nbsp;— 1,3 килобайт, что почти в&amp;nbsp;5 раз короче.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/browsers/&quot;&gt;браузеры&lt;/a&gt;, &lt;a href=&quot;https://parpalak.com/blog/keywords/upmath/&quot;&gt;upmath&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/07/01/native_compression_in_js</guid>
				<pubDate>Tue, 01 Jul 2025 09:46:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/07/01/native_compression_in_js#comment</comments>
			</item>
			<item>
				<title>Разбираем конечный автомат в системе личных сообщений</title>
				<link>https://parpalak.com/blog/2025/06/26/state_machine_in_private_messages</link>
				<description>&lt;p&gt;В&amp;nbsp;прошлый раз я рассказывал о&amp;nbsp;&lt;a href=&quot;https://parpalak.com/blog/2024/07/09/state_machines_in_programming&quot;&gt;применении понятия конечного автомата в&amp;nbsp;программировании&lt;/a&gt;. В&amp;nbsp;этот раз рассмотрим практический пример.&lt;/p&gt;
&lt;p&gt;Одним из первых моих заданий в&amp;nbsp;&lt;a href=&quot;https://parpalak.com/blog/2008/11/10/PunBB_1_3&quot;&gt;команде форума PunBB&lt;/a&gt; была &lt;a href=&quot;https://punbb.informer.com/wiki/punbb13/extensions/pun_pm&quot;&gt;система обмена личными сообщениями&lt;/a&gt;, которую я проектировал и&amp;nbsp;разрабатывал с&amp;nbsp;нуля. На&amp;nbsp;примере этой системы посмотрим, как требования к&amp;nbsp;системе преобразуются в&amp;nbsp;набор состояний и&amp;nbsp;переходов между ними.&lt;/p&gt;
&lt;h3&gt;Требование №1: черновики и&amp;nbsp;уведомления о&amp;nbsp;прочтении&lt;/h3&gt;
&lt;p&gt;Когда один пользователь отправляет другому сообщения внутри &lt;nobr&gt;какой-то&lt;/nobr&gt; системы, логично предложить следующие статусы сообщений: &lt;em&gt;черновик&lt;/em&gt;, &lt;em&gt;отправлено&lt;/em&gt;, &lt;em&gt;прочитано&lt;/em&gt;. Этих статусов достаточно, чтобы запрограммировать как сохранение сообщений в&amp;nbsp;черновики, так и&amp;nbsp;отображение отправителю отметки о&amp;nbsp;прочтении.&lt;/p&gt;
&lt;p&gt;&lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/%5Cusetikzlibrary%7Barrows%7D%0D%0A%5Cbegin%7Btikzpicture%7D%5Bnode%20distance%3D4cm%2Cfont%3D%5Csffamily%5D%0D%0A%5Ctikzset%7B%0D%0A%20%20%20%20mynode%2F.style%3D%7Brectangle%2Crounded%20corners%2Cdraw%3Dblack%2Cthick%2C%20inner%20sep%3D0.7em%2C%20text%20width%3D7em%2Ctext%20centered%7D%2C%0D%0A%20%20%20%20myarrow%2F.style%3D%7B-%3E%2C%20%3E%3Dlatex&amp;#039;%2C%20shorten%20%3E%3D1pt%2C%20shorten%20%3C%3D2pt%2Cthick%2Cfont%3D%5Csmall%5Csffamily%7D%0D%0A%7D%20%20%0D%0A%5Cnode%5Bmynode%2Cfill%3Dgray!10%5D%20(Draft)%20%7B%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%5C%5Cstatus%3Ddraft%7D%3B%20%20%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDraft%2Cfill%3Dcyan!10%5D%20(Sent)%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Dsent%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DSent%2Cfill%3Dgreen!10%5D%20(Read)%20%7B%D0%9F%D1%80%D0%BE%D1%87%D0%B8%D1%82%D0%B0%D0%BD%D0%BE%5C%5Cstatus%3Dread%7D%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Draft)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%C2%BB%7D%20%20(Sent)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%7D%20(Read)%3B%0D%0A%5Cend%7Btikzpicture%7D&quot; alt=&quot;\usetikzlibrary{arrows}
\begin{tikzpicture}[node distance=4cm,font=\sffamily]
\tikzset{
    mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered},
    myarrow/.style={-&amp;gt;, &amp;gt;=latex&amp;#039;, shorten &amp;gt;=1pt, shorten &amp;lt;=2pt,thick,font=\small\sffamily}
}  
\node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft};  
\node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent};
\node[mynode, right of=Sent,fill=green!10] (Read) {Прочитано\\status=read};
\draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»}  (Sent);
\draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read);
\end{tikzpicture}&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Требование №2: отзыв отправленных сообщений&lt;/h3&gt;
&lt;p&gt;Кроме очевидной функциональности мы решили добавить уникальную по тем временам фичу&amp;nbsp;— отзыв сообщения. Если получатель не заходил на форум и&amp;nbsp;не имел возможности узнать о&amp;nbsp;том, что ему пришло сообщение, отправитель мог бесследно отозвать это сообщение, даже если после отправки прошло много времени.&lt;/p&gt;
&lt;p&gt;Чтобы дать отправителю возможность отзывать сообщения, нужно не только разрешить обратный переход из «&lt;em&gt;отправлено&lt;/em&gt;» в&amp;nbsp;«&lt;em&gt;черновик&lt;/em&gt;», но и&amp;nbsp;добавить дополнительный статус «&lt;em&gt;доставлено&lt;/em&gt;». Переход к&amp;nbsp;нему происходит в&amp;nbsp;тот момент, когда отправитель может узнать о&amp;nbsp;том, что ему пришло новое сообщение. (В&amp;nbsp;зависимости от настройки форума это либо посещение любой страницы, если количество непрочитанных сообщений отображается в&amp;nbsp;меню, либо переход непосредственно ко входящим сообщениям.)&lt;/p&gt;
&lt;p&gt;&lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/%5Cusetikzlibrary%7Barrows%7D%0D%0A%5Cbegin%7Btikzpicture%7D%5Bnode%20distance%3D4cm%2Cfont%3D%5Csffamily%5D%0D%0A%5Ctikzset%7B%0D%0A%20%20%20%20mynode%2F.style%3D%7Brectangle%2Crounded%20corners%2Cdraw%3Dblack%2Cthick%2C%20inner%20sep%3D0.7em%2C%20text%20width%3D7em%2Ctext%20centered%7D%2C%0D%0A%20%20%20%20myarrow%2F.style%3D%7B-%3E%2C%20%3E%3Dlatex&amp;#039;%2C%20shorten%20%3E%3D1pt%2C%20shorten%20%3C%3D2pt%2Cthick%2Cfont%3D%5Csmall%5Csffamily%7D%0D%0A%7D%20%20%0D%0A%5Cnode%5Bmynode%2Cfill%3Dgray!10%5D%20(Draft)%20%7B%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%5C%5Cstatus%3Ddraft%7D%3B%20%20%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDraft%2Cfill%3Dcyan!10%5D%20(Sent)%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Dsent%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DSent%2Cfill%3Dgreen!10%5D%20(Delivered)%20%7B%D0%94%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Ddelivered%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDelivered%2Cfill%3Dgreen!10%5D%20(Read)%20%7B%D0%9F%D1%80%D0%BE%D1%87%D0%B8%D1%82%D0%B0%D0%BD%D0%BE%5C%5Cstatus%3Dread%7D%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Draft)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%C2%BB%7D%20%20(Sent)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D-50%2Cout%3D-130%5D%20node%5Bbelow%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%92%20%D1%87%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%D0%B8%C2%BB%7D%20(Draft)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B2%D0%B8%D0%B4%D0%B5%D0%BB%5C%5C%20%D0%BA%D0%BE%D0%BB-%D0%B2%D0%BE%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9%7D%20(Delivered)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Delivered)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%7D%20(Read)%3B%0D%0A%5Cend%7Btikzpicture%7D&quot; alt=&quot;\usetikzlibrary{arrows}
\begin{tikzpicture}[node distance=4cm,font=\sffamily]
\tikzset{
    mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered},
    myarrow/.style={-&amp;gt;, &amp;gt;=latex&amp;#039;, shorten &amp;gt;=1pt, shorten &amp;lt;=2pt,thick,font=\small\sffamily}
}  
\node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft};  
\node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent};
\node[mynode, right of=Sent,fill=green!10] (Delivered) {Доставлено\\status=delivered};
\node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read};
\draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»}  (Sent);
\draw[myarrow] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель нажал\\«В черновики»} (Draft);
\draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered);
\draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read);
\end{tikzpicture}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Если&amp;nbsp;бы требовалось сделать отправку уведомлений о&amp;nbsp;новых личных сообщениях по электронной почте, пришлось&amp;nbsp;бы делать переход «&lt;em&gt;отправлено&lt;/em&gt;» → «&lt;em&gt;доставлено&lt;/em&gt;» в&amp;nbsp;момент отправки письма, так как мы уже не контролируем процесс доставки этого письма и&amp;nbsp;не знаем, когда оно будет прочитано.&lt;/p&gt;
&lt;h3&gt;Требование №3: удаление сообщений&lt;/h3&gt;
&lt;p&gt;По&amp;nbsp;требованиям и&amp;nbsp;получатель, и&amp;nbsp;отправитель могут удалять сообщения. В&amp;nbsp;свое время я сделал два флага: &lt;code&gt;deleted_by_sender&lt;/code&gt; и&amp;nbsp;&lt;code&gt;deleted_by_receiver&lt;/code&gt;. Они нужны для того, чтобы знать, от кого из участников уже надо скрыть сообщение, а&amp;nbsp;от кого еще нет. Если&amp;nbsp;же они оба удаляют сообщение, то оно удаляется из базы данных целиком. (Здесь важно не повторить мою ошибку и&amp;nbsp;устанавливать флаги в&amp;nbsp;транзакции после блокировки строк, иначе при одновременном удалении отправителем и&amp;nbsp;получателем получим оба установленных флага вместо полного удаления сообщения.)&lt;/p&gt;
&lt;p&gt;С&amp;nbsp;одной стороны флаги &lt;code&gt;deleted_by_sender&lt;/code&gt; и&amp;nbsp;&lt;code&gt;deleted_by_receiver&lt;/code&gt; выглядят красиво и&amp;nbsp;симметрично. Но&amp;nbsp;с&amp;nbsp;другой стороны, если вы внимательно читали &lt;a href=&quot;https://parpalak.com/blog/2024/07/09/state_machines_in_programming&quot;&gt;предыдущий пост&lt;/a&gt;, то уже догадались, что набор из трех полей (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;deleted_by_sender&lt;/code&gt; и&amp;nbsp;&lt;code&gt;deleted_by_receiver&lt;/code&gt;)&amp;nbsp;— не самое лучшее решение для кодирования состояния.&lt;/p&gt;
&lt;p&gt;Первая проблема этих флагов и&amp;nbsp;поля состояния заключается в&amp;nbsp;том, что некоторые наборы значений (например, &lt;code&gt;status = draft&lt;/code&gt; и&amp;nbsp;&lt;code&gt;deleted_by_receiver = 1&lt;/code&gt;) не соответствуют ни одному допустимому состоянию (черновик не может иметь отметку об удалении получателем, потому что получатель ничего не получал). Вторая проблема проявляется в&amp;nbsp;повторении в&amp;nbsp;коде одних и&amp;nbsp;тех&amp;nbsp;же условий. Так, условие &lt;code&gt;deleted_by_receiver = 0 AND (status = &#039;delivered&#039; OR status = &#039;read&#039;)&lt;/code&gt;, которое соответствует доступным получателю сообщениям, повторяется &lt;a href=&quot;https://github.com/parpalak/punbb/blob/1.3/extensions/pun_pm/functions.php&quot;&gt;в коде&lt;/a&gt; три раза.&lt;/p&gt;
&lt;p&gt;Что&amp;nbsp;же делать с&amp;nbsp;признаками удаления сообщений? На&amp;nbsp;диаграмме состояний видно, что статусами управляет сначала отправитель, а&amp;nbsp;потом получатель. Также получатель может удалить сообщение только после его доставки. Поэтому состояние «удалено получателем» вполне естественно вписывается в&amp;nbsp;имеющийся набор состояний:&lt;/p&gt;
&lt;p&gt;&lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/%5Cusetikzlibrary%7Barrows%7D%0D%0A%5Cbegin%7Btikzpicture%7D%5Bnode%20distance%3D3.6cm%2Cfont%3D%5Csffamily%5D%0D%0A%5Ctikzset%7B%0D%0A%20%20%20%20mynode%2F.style%3D%7Brectangle%2Crounded%20corners%2Cdraw%3Dblack%2Cthick%2C%20inner%20sep%3D0.5em%2C%20text%20width%3D7em%2Ctext%20centered%7D%2C%0D%0A%20%20%20%20myarrow%2F.style%3D%7B-%3E%2C%20%3E%3Dlatex&amp;#039;%2C%20shorten%20%3E%3D1pt%2C%20shorten%20%3C%3D2pt%2Cthick%2Cfont%3D%5Csmall%5Csffamily%7D%0D%0A%7D%20%20%0D%0A%5Cnode%5Bmynode%2Cfill%3Dgray!10%5D%20(Draft)%20%7B%5Cshortstack%7B%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%5C%5Cstatus%3Ddraft%7D%7D%3B%20%20%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDraft%2Cfill%3Dcyan!10%5D%20(Sent)%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Dsent%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DSent%2Cfill%3Dgreen!10%5D%20(Delivered)%20%7B%5Cshortstack%7B%D0%94%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Ddelivered%7D%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDelivered%2Cfill%3Dgreen!10%5D%20(Read)%20%7B%D0%9F%D1%80%D0%BE%D1%87%D0%B8%D1%82%D0%B0%D0%BD%D0%BE%5C%5Cstatus%3Dread%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DRead%2Cfill%3Dred!10%5D%20(Deleted)%20%7B%D0%A3%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Ddeleted%7D%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Draft)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%C2%BB%7D%20%20(Sent)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D-50%2Cout%3D-130%5D%20node%5Bbelow%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%92%20%D1%87%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%D0%B8%C2%BB%7D%20(Draft)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B2%D0%B8%D0%B4%D0%B5%D0%BB%5C%5C%20%D0%BA%D0%BE%D0%BB-%D0%B2%D0%BE%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9%7D%20(Delivered)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Delivered)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%7D%20(Read)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Read)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5%7D%20(Deleted)%3B%0D%0A%5Cend%7Btikzpicture%7D&quot; alt=&quot;\usetikzlibrary{arrows}
\begin{tikzpicture}[node distance=3.6cm,font=\sffamily]
\tikzset{
    mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.5em, text width=7em,text centered},
    myarrow/.style={-&amp;gt;, &amp;gt;=latex&amp;#039;, shorten &amp;gt;=1pt, shorten &amp;lt;=2pt,thick,font=\small\sffamily}
}  
\node[mynode,fill=gray!10] (Draft) {\shortstack{Черновик\\status=draft}};  
\node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent};
\node[mynode, right of=Sent,fill=green!10] (Delivered) {\shortstack{Доставлено\\status=delivered}};
\node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read};
\node[mynode, right of=Read,fill=red!10] (Deleted) {Удалено\\status=deleted};
\draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»}  (Sent);
\draw[myarrow] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель нажал\\«В черновики»} (Draft);
\draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered);
\draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read);
\draw[myarrow] (Read) to[in=130,out=50] node[above,align=center] {Получатель удалил\\ сообщение} (Deleted);
\end{tikzpicture}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;К&amp;nbsp;сожалению, от флага &lt;code&gt;deleted_by_sender&lt;/code&gt; не получится избавиться так&amp;nbsp;же просто. Дело в&amp;nbsp;том, что удаление сообщения отправителем может произойти в&amp;nbsp;любом статусе, и&amp;nbsp;у&amp;nbsp;этого действия будут разные последствия:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;при удалении черновика или отправленного сообщения запись удаляется из базы данных;&lt;/li&gt;
&lt;li&gt;при удалении доставленного или прочитанного сообщения мы скрываем его от отправителя;&lt;/li&gt;
&lt;li&gt;при удалении отправителем уже удаленного получателем сообщения запись также удаляется из базы данных.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;С&amp;nbsp;учетом этих требований диаграмма всех возможных состояний приобретает следующий вид:&lt;/p&gt;
&lt;p&gt;&lt;img border=&quot;0&quot; style=&quot;vertical-align: middle;&quot; src=&quot;//i.upmath.me/svg/%5Cusetikzlibrary%7Barrows%7D%0D%0A%5Cbegin%7Btikzpicture%7D%5Bnode%20distance%3D3.6cm%2Cfont%3D%5Csffamily%5D%0D%0A%5Ctikzset%7B%0D%0A%20%20%20%20mynode%2F.style%3D%7Brectangle%2Crounded%20corners%2Cdraw%3Dblack%2Cthick%2C%20inner%20sep%3D0.5em%2C%20text%20width%3D7.0em%2Ctext%20centered%7D%2C%0D%0A%20%20%20%20myarrow%2F.style%3D%7B-%3E%2C%20%3E%3Dlatex&amp;#039;%2C%20shorten%20%3E%3D1pt%2C%20shorten%20%3C%3D2pt%2Cthick%2Corange!70!black%2Cfont%3D%5Csmall%5Csffamily%7D%2C%0D%0A%20%20%20%20sender%2F.style%3D%7Bblue!80!black%7D%0D%0A%7D%0D%0A%5Cclip%20(-10%2C10)%20rectangle%20(20%2C-8)%3B%20%25%D0%BA%D0%BE%D1%81%D1%82%D1%8B%D0%BB%D1%8C%0D%0A%5Cnode%5Bmynode%2Cfill%3Dgray!10%5D%20(Draft)%20%7B%D0%A7%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%5C%5Cstatus%3Ddraft%7D%3B%20%20%0D%0A%5Cnode%5Bmynode%2Cabove%20of%3DDraft%2Cdashed%5D%20(New)%20%7B%D0%92%20%D0%91%D0%94%20%D0%BF%D1%83%D1%81%D1%82%D0%BE%7D%3B%20%20%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDraft%2Cfill%3Dcyan!10%5D%20(Sent)%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Dsent%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DSent%2Cfill%3Dgreen!10%5D%20(Delivered)%20%7B%D0%94%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Ddelivered%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20below%20of%3DDelivered%2Cfill%3Dyellow!10%5D%20(DeliveredAndDeleted)%20%7B%5Cshortstack%7B%D0%94%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BB.%20%D0%B8%20%D1%83%D0%B4%D0%B0%D0%BB.%7D%5C%5Cstatus%3Ddelivered%5C%5Cdel%5C_by%5C_sendr%3D1%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DDelivered%2Cfill%3Dgreen!10%5D%20(Read)%20%7B%D0%9F%D1%80%D0%BE%D1%87%D0%B8%D1%82%D0%B0%D0%BD%D0%BE%5C%5Cstatus%3Dread%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20below%20of%3DRead%2Cfill%3Dyellow!10%5D%20(ReadAndDeleted)%20%7B%D0%9F%D1%80%D0%BE%D1%87%D0%B8%D1%82.%20%D0%B8%20%D1%83%D0%B4%D0%B0%D0%BB.%5C%5Cstatus%3Dread%5C%5Cdel%5C_by%5C_sendr%3D1%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20right%20of%3DRead%2Cfill%3Dyellow!10%5D%20(Deleted)%20%7B%D0%A3%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%BE%5C%5Cstatus%3Ddeleted%7D%3B%0D%0A%5Cnode%5Bmynode%2C%20below%20of%3DDeleted%2Cdashed%5D%20(Deleted2)%20%7B%D0%A3%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%BE%20%D0%B8%D0%B7%20%D0%91%D0%94%7D%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(New)%20to%5B%5D%20node%5Bleft%2Calign%3Dright%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%92%20%D1%87%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%D0%B8%C2%BB%7D%20%20(Draft)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(New)%20to%5Bout%3D0%2Cin%3D90%5D%20node%5Bright%2Cpos%3D0.2%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%C2%BB%7D%20(Sent)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Draft)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%C2%BB%7D%20%20(Sent)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Sent)%20to%5Bin%3D-50%2Cout%3D-130%5D%20node%5Bbelow%2Calign%3Dcenter%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D0%BD%D0%B0%D0%B6%D0%B0%D0%BB%5C%5C%C2%AB%D0%92%20%D1%87%D0%B5%D1%80%D0%BD%D0%BE%D0%B2%D0%B8%D0%BA%D0%B8%C2%BB%7D%20(Draft)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Sent)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B2%D0%B8%D0%B4%D0%B5%D0%BB%5C%5C%20%D0%BA%D0%BE%D0%BB-%D0%B2%D0%BE%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9%7D%20(Delivered)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Delivered)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Read)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(Read)%20to%5Bin%3D130%2Cout%3D50%5D%20node%5Babove%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Deleted)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Delivered)%20to%20node%5Bleft%2Calign%3Dright%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(DeliveredAndDeleted)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Read)%20to%20node%5Bleft%2Calign%3Dright%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(ReadAndDeleted)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Deleted)%20to%20node%5Bleft%2Calign%3Dright%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%5C%5C%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Deleted2)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Draft)%20to%5Bin%3D-80%2Cout%3D-90%2Clooseness%3D1.2%5D%20node%5Bbelow%2Cpos%3D0.6%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Deleted2)%3B%0D%0A%5Cdraw%5Bmyarrow%2Csender%5D%20(Sent)%20to%5Bin%3D-90%2Cout%3D-90%2Clooseness%3D1.3%5D%20node%5Bbelow%2Cpos%3D0.58%5D%20%7B%D0%9E%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Deleted2)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(DeliveredAndDeleted)%20to%5Bin%3D-130%2Cout%3D-50%5D%20node%5Bbelow%2Calign%3Dcenter%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(ReadAndDeleted)%3B%0D%0A%5Cdraw%5Bmyarrow%5D%20(ReadAndDeleted)%20to%5Bin%3D-130%2Cout%3D-50%5D%20node%5Bbelow%2Calign%3Dcenter%2Cpos%3D0.4%5D%20%7B%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%20%D1%83%D0%B4%D0%B0%D0%BB%D0%B8%D0%BB%5C%5C%20%D1%81%D0%BE%D0%BE%D0%B1%D1%89.%7D%20(Deleted2)%3B%0D%0A%5Cend%7Btikzpicture%7D&quot; alt=&quot;\usetikzlibrary{arrows}
\begin{tikzpicture}[node distance=3.6cm,font=\sffamily]
\tikzset{
    mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.5em, text width=7.0em,text centered},
    myarrow/.style={-&amp;gt;, &amp;gt;=latex&amp;#039;, shorten &amp;gt;=1pt, shorten &amp;lt;=2pt,thick,orange!70!black,font=\small\sffamily},
    sender/.style={blue!80!black}
}
\clip (-10,10) rectangle (20,-8); %костыль
\node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft};  
\node[mynode,above of=Draft,dashed] (New) {В БД пусто};  
\node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent};
\node[mynode, right of=Sent,fill=green!10] (Delivered) {Доставлено\\status=delivered};
\node[mynode, below of=Delivered,fill=yellow!10] (DeliveredAndDeleted) {\shortstack{Доставл. и удал.}\\status=delivered\\del\_by\_sendr=1};
\node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read};
\node[mynode, below of=Read,fill=yellow!10] (ReadAndDeleted) {Прочит. и удал.\\status=read\\del\_by\_sendr=1};
\node[mynode, right of=Read,fill=yellow!10] (Deleted) {Удалено\\status=deleted};
\node[mynode, below of=Deleted,dashed] (Deleted2) {Удалено из БД};
\draw[myarrow,sender] (New) to[] node[left,align=right] {Отправитель\\нажал\\«В черновики»}  (Draft);
\draw[myarrow,sender] (New) to[out=0,in=90] node[right,pos=0.2,align=center] {Отправитель нажал\\«Отправить»} (Sent);
\draw[myarrow,sender] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель\\нажал\\«Отправить»}  (Sent);
\draw[myarrow,sender] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель\\нажал\\«В черновики»} (Draft);
\draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered);
\draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщ.} (Read);
\draw[myarrow] (Read) to[in=130,out=50] node[above,align=center] {Получатель удалил\\ сообщ.} (Deleted);
\draw[myarrow,sender] (Delivered) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (DeliveredAndDeleted);
\draw[myarrow,sender] (Read) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (ReadAndDeleted);
\draw[myarrow,sender] (Deleted) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (Deleted2);
\draw[myarrow,sender] (Draft) to[in=-80,out=-90,looseness=1.2] node[below,pos=0.6] {Отправитель удалил сообщ.} (Deleted2);
\draw[myarrow,sender] (Sent) to[in=-90,out=-90,looseness=1.3] node[below,pos=0.58] {Отправитель удалил сообщ.} (Deleted2);
\draw[myarrow] (DeliveredAndDeleted) to[in=-130,out=-50] node[below,align=center] {Получатель открыл\\ сообщ.} (ReadAndDeleted);
\draw[myarrow] (ReadAndDeleted) to[in=-130,out=-50] node[below,align=center,pos=0.4] {Получатель удалил\\ сообщ.} (Deleted2);
\end{tikzpicture}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Я добавил на диаграмму начальное состояние, когда в&amp;nbsp;базе данных еще нет сообщения и&amp;nbsp;отправитель сохраняет его впервые, а&amp;nbsp;также конечное состояние, когда оба участника переписки удалили сообщение. В&amp;nbsp;принципе, никто не запрещает ввести отдельные коды для состояний «&lt;em&gt;доставлено получателю и&amp;nbsp;удалено отправителем&lt;/em&gt;» и&amp;nbsp;«&lt;em&gt;прочитано получателем и&amp;nbsp;удалено отправителем&lt;/em&gt;», чтобы состояние кодировалось всего одним полем &lt;code&gt;status&lt;/code&gt;. Но&amp;nbsp;я&amp;nbsp;бы оставил флаг &lt;code&gt;deleted_by_sender&lt;/code&gt; отдельно от поля &lt;code&gt;status&lt;/code&gt;, чтобы смысл данных в&amp;nbsp;таблице был интуитивно понятным.&lt;/p&gt;
&lt;h3&gt;Выводы и&amp;nbsp;анализ корректности требований&lt;/h3&gt;
&lt;p&gt;Я показал, как можно анализировать &lt;nobr&gt;бизнес-требования&lt;/nobr&gt; с&amp;nbsp;помощью диаграммы состояний. Она отражает полный жизненный цикл некоторой сущности: в&amp;nbsp;каких состояниях эта сущность может находиться, какие переходы между ними возможны и&amp;nbsp;какие пользователи отвечают за эти переходы. По&amp;nbsp;ходу составления мы можем проверять, насколько полна и&amp;nbsp;корректна получающаяся диаграмма.&lt;/p&gt;
&lt;p&gt;Проанализируем корректность и&amp;nbsp;полноту диаграммы из нашего примера. На&amp;nbsp;диаграмме отсутствует переход между доставленным и&amp;nbsp;удаленным состоянием сообщения. Должен&amp;nbsp;ли он существовать? Должен, если получатель может удалить сообщение из списка, не открывая его. Должна&amp;nbsp;ли система отображать отправителю, что получатель, не прочитав, удалил сообщение? Если должна, то одного состояния «&lt;em&gt;удалено&lt;/em&gt;» недостаточно, так как мы теряем информацию о&amp;nbsp;прочтении удаленных сообщений. Возможно, в&amp;nbsp;этом случае не стоило удалять флаг &lt;code&gt;deleted_by_receiver&lt;/code&gt; и&amp;nbsp;заменять его на &lt;code&gt;status=deleted&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Аналогичными вопросами проверяется корректность всей диаграммы состояний на этапе проектирования. Их можно задавать и&amp;nbsp;самому себе, и&amp;nbsp;постановщику задачи для полного прояснения требований.&lt;/p&gt;&lt;p class=&quot;article_tags&quot;&gt;
	Ключевые слова:
	&lt;a href=&quot;https://parpalak.com/blog/keywords/programming/&quot;&gt;программирование&lt;/a&gt;&lt;/p&gt;</description>
				<dc:creator>Роман Парпалак</dc:creator>
				<guid isPermaLink="true">https://parpalak.com/blog/2025/06/26/state_machine_in_private_messages</guid>
				<pubDate>Thu, 26 Jun 2025 18:04:00 GMT</pubDate>
				<comments>https://parpalak.com/blog/2025/06/26/state_machine_in_private_messages#comment</comments>
			</item>
		</channel>
	</rss>
