<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Kirill Loskutov 🍏</title><generator>teletype.in</generator><description><![CDATA[Kirill Loskutov 🍏]]></description><image><url>https://img1.teletype.in/files/05/87/05876aa4-0ec2-4ca9-9ea9-8c1584d9bfdf.png</url><title>Kirill Loskutov 🍏</title><link>https://teletype.in/@loskir</link></image><link>https://teletype.in/@loskir?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/loskir?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/loskir?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Sun, 04 Oct 2026 05:20:18 GMT</pubDate><lastBuildDate>Sun, 04 Oct 2026 05:20:18 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@loskir/price-monitor-db</guid><link>https://teletype.in/@loskir/price-monitor-db?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir</link><comments>https://teletype.in/@loskir/price-monitor-db?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir#comments</comments><dc:creator>loskir</dc:creator><title>Как устроен Price Monitor. Работа с данными</title><pubDate>Sat, 03 Dec 2022 13:11:34 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/7d/4d/7d4ddf49-967d-46a4-ae96-fc7470f083d1.png"></media:content><description><![CDATA[<img src="https://img4.teletype.in/files/b9/4b/b94b3659-8a6f-4a12-a07b-91aa720bb664.png"></img>В этой статье я расскажу, как устроено хранение данных в Price Monitor, как я научился запрашивать сложные структуры одним запросом в базу и как я сделал так, чтобы с этим было удобно работать в коде.]]></description><content:encoded><![CDATA[
  <blockquote id="TTFi">В этой статье я расскажу, как устроено хранение данных в Price Monitor, как я научился запрашивать сложные структуры одним запросом в базу и как я сделал так, чтобы с этим было удобно работать в коде.</blockquote>
  <p id="EY5f"></p>
  <p id="JVUd">В Price Monitor я использую PostgreSQL, реляционную СУБД. Данные в ней хранятся в виде таблиц. </p>
  <p id="95z1">У меня есть таблица для наименований <code>Product</code>, таблицы для товаров каждого магазина <code>GlobusProduct/LentaProduct/...</code>, таблицы для связей товаров и наименований <code>ProductGlobusLink/ProductLentaLink/...</code>, таблица для штрих-кодов <code>ProductEan</code>, материализованные view для актуальных цен <code>GlobusProductLastPrice/LentaProductLastPrice/...</code>, несколько обычных view для удобства (например, <code>ProductWithLinks</code>) и много чего ещё. И из этого зоопарка таблиц нужно уметь доставать нужные данные.</p>
  <p id="0O4v">Результатом SELECT-запросов тоже является таблица. Это значит, что результат будет представлен в виде набора столбцов и набора строк, без каких-либо вложенных полей. Но часто хочется уметь вытаскивать одним SQL-запросом какую-нибудь сложную структуру данных (например, всю информацию о товаре и массив изменений цен во времени). На первый взгляд кажется, что это невозможно, но я нашёл интересное решение.</p>
  <p id="wxSr">В PostgreSQL есть неплохой набор инструментов для работы с JSON. В ячейках таблицы можно хранить произвольные JSON-структуры, обращаться к вложенным полям с помощью операторов <code>-&gt;</code> и <code>-&gt;&gt;</code> и даже индексировать их. А ещё там есть функция <code>json_agg</code>, которая позволяет преобразовать любую таблицу в json массив объектов.</p>
  <p id="816Z">Например:</p>
  <pre id="N9C7"> a | b
-------
 1 | 2
 3 | 4</pre>
  <p id="GbuS"><code>&gt; select json_agg(t) from t;</code></p>
  <p id="FKNL"><code>[{&quot;a&quot;: 1, &quot;b&quot;: 2}, {&quot;a&quot;: 3, &quot;b&quot;: 4}]</code></p>
  <p id="P3S0">Если эту функцию использовать в подзапросе, то в результате получается ровно такое поведение, какое мне хотелось. Если нужно достать одиночное значение, то подзапрос будет выглядеть вот так <code>(SELECT json_agg(t) -&gt; 0 FROM (подзапрос) t)</code>, если нужен весь массив целиком, то так <code>(SELECT COALESCE(json_agg(t), &#x27;[]&#x27;::json) as j FROM (подзапрос) t)</code>. coalesce нужен, чтобы возвращался пустой массив, если в подзапросе ничего не нашлось.</p>
  <p id="nFup"></p>
  <p id="YJjH">Теперь плавно перейдём к коду. Весь бэкенд Price Monitor написан на Typescript. Для работы с базой из кода я использую библиотеку kysely. Это небольшой, но мощный query builder со строгой типизацией запросов. Сначала нужно описать схему базы данных в виде типов, а потом можно строить запросы вот в таком виде:</p>
  <figure id="6R6m" class="m_retina">
    <img src="https://img4.teletype.in/files/b9/4b/b94b3659-8a6f-4a12-a07b-91aa720bb664.png" width="398" />
  </figure>
  <p id="hlrT">И если допустить ошибку в запросе, она тут же её подсветит:</p>
  <figure id="1NQI" class="m_retina">
    <img src="https://img4.teletype.in/files/b7/c4/b7c4d9eb-1321-4fdf-a4e8-68b59ed5a81e.png" width="380" />
  </figure>
  <p id="Sfnr">И, что самое главное, она позволяет выносить куски логики в отдельные функции, не теряя при этом типизации. И не только всякие простые куски кода, чтобы не копипастить их несколько раз, но и целые слои абстракции. Например, я написал утилиты для работы с json, о которых рассказал выше:</p>
  <figure id="ylyj" class="m_retina">
    <img src="https://img1.teletype.in/files/05/c6/05c62f87-b3dd-4f6e-a723-20f557ae5fb1.png" width="615" />
  </figure>
  <p id="mNE0">Выглядит как магия. Полностью кастомный кусок SQL, который идеально интегрируется в запросы и сохраняет при этом типизацию!</p>
  <p id="hpV5">Я использую эти функции во многих местах, но, в частности, в функции для поиска товара:</p>
  <figure id="mM4O" class="m_retina">
    <img src="https://img4.teletype.in/files/b9/e4/b9e4fc4b-b232-412f-9ff1-36a62d7518d0.png" width="732" />
  </figure>
  <p id="pwVb">eans тут достаются в виде массива, а товары из разных магазинов — в виде одиночных объектов. Второе необязательно, но очень упрощает дальнейшую обработку.</p>
  <p id="s2v5">Для выборки товаров по условию у меня тоже есть отдельные функции: по id, по ean и по категории (но она не влезла в кадр)</p>
  <figure id="mSRw" class="m_retina">
    <img src="https://img1.teletype.in/files/89/3a/893a4358-5fc3-4b31-862b-d09960085897.png" width="698" />
  </figure>
  <p id="trDd">И теперь, чтобы получить товар по id, можно просто написать...</p>
  <figure id="FUsT" class="m_retina">
    <img src="https://img3.teletype.in/files/ad/b8/adb85db3-1a8b-434c-9868-d417ed6b800c.png" width="334" />
  </figure>
  <p id="bDqr">Kysely поддерживает не только вот такие махинации с запросами, но и стриминг результатов, транзакции и много чего ещё. Очень доволен этой библиотекой.</p>
  <blockquote id="j32R">Спасибо, что прочитали! Надеюсь, стало понятнее, как устроен Price Monitor. В следующей части (когда-нибудь) расскажу, как я объединял товары друг с другом.</blockquote>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@loskir/type-level-addition</guid><link>https://teletype.in/@loskir/type-level-addition?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir</link><comments>https://teletype.in/@loskir/type-level-addition?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir#comments</comments><dc:creator>loskir</dc:creator><title>Складываем числа в системе типов Typescript</title><pubDate>Sun, 13 Nov 2022 13:20:28 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/5c/0e/5c0e4e62-6e01-4d33-957e-3b602bb774f5.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/48/ba/48ba17b3-e664-44b3-8928-1fe93ed4efef.png"></img>Недавно я участвовал в соревновании Yandex Cup, где одним из заданий в треке фронтенд-разработки было решить некую задачку полностью в рамках системы типов Typescript. Я очень люблю тайпскрипт в целом и его систему типов в частности, поэтому на это задание я потратил значительную часть всего времени. Что там было — совсем другая история, но именно тогда мне пришла идея написать свой сумматор на типах.]]></description><content:encoded><![CDATA[
  <figure id="Pwml" class="m_retina">
    <img src="https://img1.teletype.in/files/48/ba/48ba17b3-e664-44b3-8928-1fe93ed4efef.png" width="510" />
  </figure>
  <p id="8Sgg">Недавно я участвовал в соревновании <a href="https://yandex.ru/cup" target="_blank">Yandex Cup</a>, где одним из заданий в треке фронтенд-разработки было решить некую задачку полностью в рамках системы типов Typescript. Я очень люблю тайпскрипт в целом и его систему типов в частности, поэтому на это задание я потратил значительную часть всего времени. Что там было — совсем другая история, но именно тогда мне пришла идея написать свой сумматор на типах.</p>
  <p id="SVSc"></p>
  <section style="background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="tQOq">В процессе решения задания я немного поискал в интернете и наткнулся на проект <a href="https://github.com/dqn/calc-ts" target="_blank">calc-ts</a>, который реализует основные арифметические операции и парсинг выражений. Но у него обнаружился один фатальный недостаток. В силу особенностей реализации, в которые я не буду вдаваться, он перестаёт работать на числах больше нескольких тысяч. Там используется довольно наивный подход &quot;давайте увеличивать одно число на единичку, а второе уменьшать, пока оно не станет равно нулю&quot;, и тайпскрипт захлёбывается на таком количестве итераций.</p>
    <figure id="RSky" class="m_retina">
      <img src="https://img2.teletype.in/files/5f/81/5f81bc19-b9a1-4447-bf81-acbbfa8e3c3d.png" width="502" />
    </figure>
  </section>
  <p id="vNfS"></p>
  <p id="jNKt">Вместо этого я решил взять за основу десятичную систему и алгоритм сложения столбиком. Что ж, начнём! </p>
  <p id="9XXa">Нам нужно реализовать тип, который принимает на вход два числа, а на выход выдаёт ещё одно число — сумму.</p>
  <p id="M6hx">Сначала нужно превратить числа в строки, а строки разбить на отдельные цифры. Это, пожалуй, единственный способ удобной работы с числами в TS, ведь никаких арифметических операций в системе типов нет.</p>
  <pre id="rA2g" data-lang="typescript">type StringToChars&lt;T extends string&gt; = T extends &#x60;${infer C}${infer Rest}&#x60;
  ? [C, ...StringToChars&lt;Rest&gt;]
  : [];
type NumToChars&lt;T extends number&gt; = StringToChars&lt;&#x60;${T}&#x60;&gt;;</pre>
  <p id="MiVB">Для преобразования числа в строку мы используем подстановку в template literal. А вот для разбиения строки на символы пригодится рекурсия и немного infer-магии. Простыми словами можно описать так:</p>
  <ul id="O3mo">
    <li id="WVaB">на каждом шаге мы проверяем, представляет ли строка из себя нечто формата <code>&#x60;${первый символ}${всё остальное}&#x60;</code> (причём всё остальное может быть пустой строкой)</li>
    <li id="mafR">если да, то возвращаем первый символ и результат разбиения оставшейся строки (которая возвращает второй символ и результат разбиения оставшейся строки и так далее)</li>
    <li id="72Hi">если нет, то возвращаем пустой массив (это нужно для завершения рекурсии, когда строка пустая)</li>
  </ul>
  <p id="GnmL">Теперь из числа <code>12345</code>  мы можем получить кортеж цифр <code>[&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;]</code>. Дальше вспоминаем алгоритм сложения столбиком: числа нужно записать так, чтобы одинаковые разряды оказались друг над другом. В нашем случае — чтобы одинаковые разряды была на одинаковых позициях в кортеже. Как бы сделать так, чтобы цифра 5 в числе 12345 была на том же месте, что и 8 в 678? Развернуть массив!</p>
  <p id="hBzR"></p>
  <p id="kaEy">Для этого опять используем infer и рекурсию:</p>
  <pre id="gggq" data-lang="typescript">type Reverse&lt;T extends any[]&gt; = T extends [infer V, ...infer Rest]
  ? [...Reverse&lt;Rest&gt;, V]
  : [];</pre>
  <p id="TYLD"><code>Reverse&lt;NumToChars&lt;12345&gt;&gt;</code> даст нам <code>[&quot;5&quot;, &quot;4&quot;, &quot;3&quot;, &quot;2&quot;, &quot;1&quot;]</code>, а <code>Reverse&lt;NumToChars&lt;678&gt;&gt;</code> — <code>[&quot;8&quot;, &quot;7&quot;, &quot;6&quot;]</code>. Как видите, разряды действительно совпадают.</p>
  <p id="8N1U"></p>
  <p id="IgoZ">Теперь нужно научиться складывать две цифры. Для этого я написал маленький скриптик, который генерирует look-up table:</p>
  <pre id="zTbE" data-lang="javascript">for (let i = 0; i &lt;= 9; ++i) {
  console.log(&#x60;A extends &#x27;${i}&#x27; ? (&#x60;)
  for (let j = 0; j &lt;= 9; ++j) {
    console.log(&#x60;B extends &#x27;${j}&#x27; ? [${i + j &gt;= 10 ? &#x60;&#x27;${(i+j) % 10}&#x27;, true&#x60;: &#x60;&#x27;${i+j}&#x27;, false&#x60;}] :&#x60;)
  }
  console.log(&#x27;never) :&#x27;)
}
console.log(&#x27;never&#x27;)</pre>
  <p id="AmIb">Результат выглядит примерно так:</p>
  <pre id="TLBj" data-lang="typescript">type AddDigitsCore&lt;A extends string, B extends string&gt; = A extends &quot;0&quot; ? (
    B extends &quot;0&quot; ? [&quot;0&quot;, false]
      : B extends &quot;1&quot; ? [&quot;1&quot;, false]
      : B extends &quot;2&quot; ? [&quot;2&quot;, false]
      // тут ещё 100 строчек…</pre>
  <p id="Scab">Тип принимает на вход два символа и на выход отдаёт символ, который нужно записать в ответ, а также нужно ли добавить единичку к следующему разряду.</p>
  <p id="jv2I"></p>
  <p id="yws6">Как добавлять единичку? Тут мне пришлось написать маленький костыль, потому что переделывать по-нормальному мне было лень.</p>
  <pre id="rE0t" data-lang="typescript">type AddOne&lt;T extends [string, boolean]&gt; = [
  AddDigitsCore&lt;T[0], &quot;1&quot;&gt;[0],
  AddDigitsCore&lt;T[0], &quot;1&quot;&gt;[1] extends true ? true : T[1],
];</pre>
  <p id="1DLw">Тип принимает на вход кортеж из символа и индикатора переполнения и отдаёт такой же кортеж на выход. Символ — это результат прибавления единички к входному символу. А вот для индикатора переполнения написано условие. Если он равен true, то возвращаем true. Иначе возвращаем то, что было передано на вход. Выглядит не очень красиво, но работает, и вот почему:</p>
  <ol id="nLTP">
    <li id="3o1e">Если на вход передаётся число с переполнением (например 12, то есть <code>[&quot;2&quot;, true]</code>), то на выходе переполнение остаётся (получается 13, то есть <code>[&quot;3&quot;, true]</code>)</li>
    <li id="UpXm">Переполнения переполнения случиться не может, потому что максимальное число на входе — это 9+9, то есть 18</li>
    <li id="exmK">Если на вход передаётся число от 0 до 8, то переполнения не появляется</li>
    <li id="9cNq">Единственный случай, когда появляется переполнение — когда передаётся 9 (<code>[&quot;9&quot;, false]</code> =&gt; <code>[&quot;0&quot;, true]</code>) </li>
  </ol>
  <p id="kgDP"></p>
  <p id="gypC">Теперь напишем тип для сложения двух цифр, но с учётом возможной «единички»:</p>
  <pre id="iisg" data-lang="typescript">type AddDigits&lt;
  A extends string,
  B extends string,
  Leftover extends boolean = false,
&gt; = Leftover extends true ? AddOne&lt;AddDigitsCore&lt;A, B&gt;&gt; : AddDigitsCore&lt;A, B&gt;;</pre>
  <p id="47Vz">Тут всё просто: если Leftover = true, то складываем цифры и добавляем единичку; если нет, то просто складываем цифры.</p>
  <p id="Yb5W"></p>
  <p id="Cv7C">Дальше напишем ещё несколько типов, которые пригодятся нам позже. <br />Первый — это тип Shift, который удаляет первый элемент из кортежа.</p>
  <pre id="1OWF" data-lang="typescript">type Shift&lt;T extends any[]&gt; = ((...v: T) =&gt; void) extends
  ((v: any, ...rest: infer V) =&gt; void) ? V : [];</pre>
  <p id="qk5F">Я не очень понимаю, зачем тут используются сигнатуры функций, но без них что-то ломается. В общем, тип «проглатывает» первый элемент, а все остальные возвращает обратно. Если массив и так пустой, то просто возвращает пустой массив.</p>
  <p id="Xi0e">Второй — это тип FallbackToZero, тут всё очевидно.</p>
  <pre id="HfK4" data-lang="typescript">type FallbackToZero&lt;T extends string | undefined&gt; = T extends undefined ? &quot;0&quot; : T;</pre>
  <p id="xl6q"></p>
  <p id="N4kS">Теперь подумаем о том, как бы нам реализовать алгоритм сложения. Шаги должны быть такие:</p>
  <ol id="cU3e">
    <li id="OMwO">Сложить два числа в текущем разряде (не забыть про возможную «единичку» сверху)</li>
    <li id="TGq7">Записать последнюю цифру результата в ответ</li>
    <li id="xZI5">Запомнить «единичку» (если она есть) для следующего разряда</li>
    <li id="iZIW">Перейти к следующему разряду</li>
  </ol>
  <p id="kqjf">В качестве решения напрашивается рекурсия, её и будем использовать. Получился тип </p>
  <pre id="AasR" data-lang="typescript">type AddCore&lt;
  A extends string[],
  B extends string[],
  Leftover extends boolean = false,
&gt; = true extends (
  &amp; (A[0] extends undefined ? true : false)
  &amp; (B[0] extends undefined ? true : false)
) ? Leftover extends true ? &quot;1&quot; : &quot;&quot;
  : &#x60;${AddCore&lt;
    Shift&lt;A&gt;,
    Shift&lt;B&gt;,
    AddDigits&lt;
      FallbackToZero&lt;A[0]&gt;,
      FallbackToZero&lt;B[0]&gt;,
      Leftover
    &gt;[1]
  &gt;}${AddDigits&lt;
    FallbackToZero&lt;A[0]&gt;,
    FallbackToZero&lt;B[0]&gt;,
    Leftover
  &gt;[0]}&#x60;;</pre>
  <p id="3qUl">Давайте разбираться, что тут происходит. На вход приходит два числа (в виде массива символов в обратном порядке) и индикатор «единички». Для первого разряда он всегда false, но для следующих итераций пригодится.</p>
  <pre id="Imx0" data-lang="typescript">AddDigits&lt;
  FallbackToZero&lt;A[0]&gt;,
  FallbackToZero&lt;B[0]&gt;,
  Leftover
&gt;[0]</pre>
  <p id="46Xd">Тут мы получаем цифру, которая идёт в ответ.</p>
  <pre id="tKso" data-lang="typescript">AddCore&lt;
  Shift&lt;A&gt;,
  Shift&lt;B&gt;,
  AddDigits&lt;
    FallbackToZero&lt;A[0]&gt;,
    FallbackToZero&lt;B[0]&gt;,
    Leftover
  &gt;[1]
&gt;</pre>
  <p id="b4KA">Тут мы запускаем рекурсию для следующего разряда. В <code>Leftover</code> передаётся индикатор переполнения из результата сложения текущего разряда. Из чисел убирается по одной цифре с помощью метода <code>Shift</code>. С каждой итерацией остаётся всё меньше и меньше цифр. Если одно число короче другого, то в один момент у него закончатся цифры, и <code>A[0]</code> (или <code>B[0]</code>) вернёт <code>undefined</code>. Тут пригождается <code>FallbackToZero</code>, который заменяет <code>undefined</code> на <code>&quot;0&quot;</code>.</p>
  <pre id="saux" data-lang="typescript">true extends (
  &amp; (A[0] extends undefined ? true : false)
  &amp; (B[0] extends undefined ? true : false)
) ? Leftover extends true ? &quot;1&quot; : &quot;&quot;</pre>
  <p id="HLA3">Тут мы проверяем, не закончились ли у нас разряды у обоих чисел сразу. Если так, то мы завершаем рекурсию и возвращаем 1, если у нас осталась «единичка», либо пустую строку.</p>
  <p id="y6xm">В результате у нас получается сумма чисел в виде строки. Никаких дополнительных манипуляций с ней производить не нужно, она уже записана в правильном порядке (старший разряд слева). Можно было попытаться возвращать результат в виде числа, но я не смог придумать оптимальный способ конвертации символов в число.</p>
  <p id="JRMM">Ну и наконец делаем юзер-френдли тип, который принимает на вход два числа, конвертирует их в обратный массив символов и вызывает <code>AddCore</code></p>
  <pre id="xsQm" data-lang="typescript">type Add&lt;A extends number, B extends number&gt; = AddCore&lt;
  Reverse&lt;NumToChars&lt;A&gt;&gt;,
  Reverse&lt;NumToChars&lt;B&gt;&gt;
&gt;;</pre>
  <p id="kWBQ">Готово! Потыкать можно в <a href="https://www.typescriptlang.org/play?#code/PTAEAcBsEME8HMBOB7ArgOwCYFgBQe8AXWcAU1ACVSBnVSQ0AXlAEFNMAeARgCYBmACwBWAGwB2ADSheggHwF8uEKACWAWyik1pdIWiEVydERLk2mACIr4KwtQDCyRKQ4tQpAB6EdmaqGqEiCro8FIAQu5ePn4BQSGyTKyR3lh+AEQADGmgAPygABR4oMWgEZ4pvqCZ2XkA2tVSAGbQkNSkALpFJcUAXKXJ0VVcNaD1w00tbZ243b395YNpPCP1yxOtHV3dfWVRqVV8K2mH61NbJTsD+2kCR7enmzOzlwvXQkfvD9OzoC97lWkREcgV9znNdhV0mIjtDQU9tvN-ukABxHVFwn5-SFVACcRzxGOeoHQpAAbqREF0AJRdPpuV4A4a5ApgxHY6rMsZpQmYtmLJl1Jbc0DNDbfH6-PnXZac47C0VneFEiGLQ6y+4iyaPCWSlXXW6yz6asWs8FXAHvWUg42KnVYxZA2Wwm3aiX267Q2Xol3i3l6gGo2UEn2m3Xm9J42VZKSBVCu3kk8mUpU0p508NVGV5QpK4r+9JZWVrEO5i5SxlHE4lu3l9JZ0Y3eVa33KjNyw1Nk2ls0M9IGwXWhXx1u9qqWwXOocthH5qqOwXeqeh90Az2C4NL7th0dpQOC6OgWPDmdtyOC8aHxBx6dlxMU86pub0pEHEY5n6zjmCqubmufgUNhqv5urWmYfJ2togZ+aoDhBx5lp+-YNpOzbLqBaTjg2i6oVuK7pPODYbjhf5tmuDYHkeN49i+u5HBelFoZ+Z4NsWDG4cSZL3imtJJDuSHvrMn6FoKQHEVBbYAfURrAX6bb1vUg5ibJO4wchcFUduNFIfU2FdiRO6YfURF6eJO4EfUFFXvB1HsmRXIxlZGl4VUe4scKbH6TRzH1FWHkgXeybdI+krPuymECd0QngTyI40ZJgLqYxckwol7HQWiqWeey2lpMZkHKTRhkNJe15JWZdHuY5ZU0XZQoOaVaVtq5PmVQ1WWLN5jb1dZkoBQ+PGhQ6b6slFVqZaZcUpTFJ47vJtHTQhbaqUZ40FdlRyWW1E1hRV3VOeh5l1SVPXOWktW+VVjU7s1XXHftTHRXdaF9dxaa8TVw2lqNE6rbF7Lxbp+V-Ysc15Sd6HLcVflrYsOX0Zd7VvEcrEI9tQ2yhdW0wx6dyteDn43Ua0PA9cnXWsTCIvUFA1NZ9H5tsJWG-TNk1Bszi2zRteP3Utu1PVdWnI9z1U7RjwsC+yh0ahTHMfR2e0i4sN3k6j2MAp1zoy3MVMlMF6Y7sxEUlN9hHszZ-JcwrEsg3zWuaeykMo1jJMAjlmP422RXS6rLv4Y9dunbVKvOyz7I3ZrPuhx1GVWzWOvFHrHFJgA3AoxBkKAADKAAWKiNIQHAACoZtA6CwLU7QJMw+T5AAdPXpJ9IXVJMAkpLIComAt72XQ143oCl7AUj17XzgBH0wSNBSoAAGot4wbcd13zIz5KFep4o6fkJngTBPAhfIPY2fQIg1BFxmsR71XoDF6OAAGAAkADek-T-YAC+z+v4glA0IQ793y6HUeww9647ziPvQ+x9T4cCoAEWQ4o+jrxMBnAAcqgNQB8j4nzPrfF86AMEACMKTX3AXvLB0Cz6PyfoXABsgN4oO3rvcAAAtCkyBcEX13vERIeDsR3wyF-dAU8f4zwASvSUhcN5b1ANg0+B8yEhHPqOQeFdr6F1qBkdoGYMCYFII0YIpBMDMjSGkHi1CNFaM-k-OR1AFHcPgBwHOecC6F1kLIABDDcAyPMAAeRJMol8tRL4hCkIQ5AyBICkFLpXRItQujmCsDYOwjhnBF00e0KQaRhiyAyRIBJ7Akm2AcE4FwljMlDDSLkrg2jRxHmZPUputQan5NwO0LxPjCnWGKRwBJXCIGtLzP0vegzQAABl9GEGQEmDM4TInRPQIkIcrTr4TPztM6edSrLMj8QExJ3SUmlNcOEdxIUunJJKWklgJyOmmF-kmNogTsSqNicwPhgxajf1nqA2uXy4GEHFHUEeVAHkuH+bIKQM9EGjHaQoZQej0DIAALSNDUIQJF1hEXOEYaAAAYi0SAhDoAAGMADWB82EoCeYMEJ8BQAAB9QC6P0YYzA6idFYBZSSYxeQORN1uRncwqSXB9NHLSiuozZzisyV0NZUyZmjjmVE0uSytQrMSPU0cAkABkBQWAZI5Xogx3KGnbL6EOYKur8hhANaOZlxqjGmrjJKC1eAW55DlRsn+WznW8qZH0Ux5jn5CqOecZx+dXAQrDbnCNYQo1Kn2Rc3ppZ8WQEJSS8lyBKXIFcBk+NPxU3prJRS9hHAbVaPzbMT1SZzjVPFB44N5yennELUS4tWbS36oraM4oraM0lqpeWyuPbxmTK9V0XJVi74CrMOwVwGYCFqGIYgcIC6iEkMSCGtJXQQUUkeegzBUCcGRsrbu0+LgD0UOPXG+QuB6F4CAA" target="_blank">песочнице</a> посмотреть код — на <a href="https://github.com/Loskir/typescript-addition" target="_blank">гитхабе</a>.</p>
  <p id="dqyh">Подписывайтесь на мой <a href="https://t.me/loskirs" target="_blank">телеграм-канал</a>, ставьте лайки, до связи!</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@loskir/grammy-1.10</guid><link>https://teletype.in/@loskir/grammy-1.10?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir</link><comments>https://teletype.in/@loskir/grammy-1.10?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir#comments</comments><dc:creator>loskir</dc:creator><title>🎉 grammY 1.10</title><pubDate>Thu, 04 Aug 2022 10:17:49 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/f4/62/f462ae53-2928-4abb-b828-fee34208a574.png"></media:content><description><![CDATA[<img src="https://img4.teletype.in/files/31/d8/31d88a99-e01e-4a79-a209-8e7b869aa349.jpeg"></img>На днях вышло большое обновление для grammY — современной typescript-библиотеки для создания телеграм-ботов.]]></description><content:encoded><![CDATA[
  <figure id="ucCv" class="m_column">
    <img src="https://img4.teletype.in/files/31/d8/31d88a99-e01e-4a79-a209-8e7b869aa349.jpeg" width="1920" />
  </figure>
  <blockquote id="Iknt">На днях вышло большое обновление для grammY — современной typescript-библиотеки для создания телеграм-ботов.</blockquote>
  <p id="I1ks">Главная тема обновления — <strong>первый стабильный релиз @grammyjs/conversations</strong>. Это библиотека, позволяющая строить сложные диалоговые взаимодействия с пользователем, почти об этом не задумываясь.</p>
  <p id="mBYa">Наверное, каждый, кто пробовал писать телеграм-ботов, хотя бы раз думал «как круто было бы, если бы можно было написать &quot;подожди следующего сообщения от пользователя и продолжи функцию&quot;». Так вот, conversations позволяют делать именно так! Например:</p>
  <pre id="8eHv" data-lang="typescript">async function greeting(conversation: MyConversation, ctx: MyContext) {
  await ctx.reply(&quot;Hi there! What is your name?&quot;);
  const { message } = await conversation.wait();
  await ctx.reply(&#x60;Welcome to the chat, ${message.text}!&#x60;);
}</pre>
  <p id="PrmV">Причём, в отличие от наивных реализаций через промисы, все данные тут хранятся в сессии, и бот получается полностью устойчив к перезагрузкам.</p>
  <p id="8TYe">И хотя я всё ещё придерживаюсь классического подхода со сценами (<a href="https://github.com/loskir/grammy-views" target="_blank">grammy-views</a>), conversations — это интересная новая абстракция, которая (наверное) будет более интуитивно понятна для начинающих разработчиков. <a href="https://grammy.dev/plugins/conversations" target="_blank">Подробнее »</a></p>
  <hr />
  <p id="exyz">Кроме этого добавили <code>ctx.has</code> — набор методов у контекста, позволяющих выполнять проверки. Раньше это было доступно только со стороны Composer&#x27;а. Например:</p>
  <pre id="StjM" data-lang="typescript">composer.command(&#x27;start&#x27;, (ctx) =&gt; ctx.reply(ctx.match.toString()))</pre>
  <p id="AHe5">Эквивалент этого с помощью ctx.has будет выглядеть примерно так:</p>
  <pre id="40fs" data-lang="typescript">composer.use((ctx, next) =&gt; {
  if (ctx.hasCommand(&#x27;start&#x27;)) {
    return ctx.reply(ctx.match.toString())
  }
  return next()
})</pre>
  <p id="uWGC">В обычных ситуациях <code>ctx.has</code> пригодится нечасто, но главное его применение — в conversations. Из-за более императивного подхода в них недоступны привычные методы Composer&#x27;а, их и заменит <code>ctx.has</code>. <a href="https://grammy.dev/guide/context.html#probing-via-has-checks" target="_blank">Подробнее »</a></p>
  <hr />
  <p id="rvaf">Ещё обновили сессии. Добавили <a href="https://grammy.dev/plugins/session.html#multi-sessions" target="_blank">мульти-сессии</a>, позволяющие хранить разные части данных в разных хранилищах; добавили «улучшатель» хранилищ, добавляющий <a href="https://grammy.dev/plugins/session.html#timeouts" target="_blank">TTL</a> для сессий и <a href="https://grammy.dev/plugins/session.html#migrations" target="_blank">инструмент</a> для миграций. Первые два кажутся мне не такими важными, а вот миграции — это правда полезно.</p>
  <p id="kis2">По мере развития бота формат данных в сессии может меняться. При этом у старых пользователей могут оставаться данные в старом формате, с которыми бот может работать неправильно. Раньше с этим приходилось бороться обходными путями: добавлять проверки на отсутствующие поля, мигрировать из одного формата в другой. </p>
  <p id="QEJ6">Новый плагин упрощает эти действия. Он позволяет создавать функции, которые мигрируют данные из одной версии формата в следующую. При этом плагин сам следит за версией данных у каждого пользователя и запускает миграции при необходимости. <a href="https://grammy.dev/plugins/session.html#migrations" target="_blank">Подробнее »</a></p>
  <hr />
  <p id="TGED">Ну и напоследок — обновили плагин для создания клавиатур. Наконец-то добавили поддержку resize_keyboard, one_time_keyboard и других полей для обычных клавиатур, без них было очень неудобно (хотя я и не пользуюсь обычными клавиатурами). А самое главное, добавили фичу, за которую я топил больше двух месяцев — конструкторы для клавиатур.</p>
  <p id="UUAM">Встроенный в grammY плагин для построения клавиатур написан в императивном стиле. Например:</p>
  <pre id="deHw" data-lang="typescript">const keyboard = new InlineKeyboard()
  .text(&#x27;1&#x27;, &#x27;a&#x27;).text(&#x27;2&#x27;, &#x27;b&#x27;).row()
  .text(&#x27;3&#x27;, &#x27;c&#x27;)</pre>
  <p id="jS4y">И это не всегда может быть удобно (например, если нужно создать клавиатуру из динамического массива)</p>
  <p id="7KyL">Поэтому я предпочитаю декларативный подход на базе обычных двумерных массивов. Я сделал плагин grammy-markup, который предоставляет функции-помощники для создания кнопок Button и IButton. Но вот незадача: в Keyboard и InlineKeyboard нельзя передать сырые данные о клавиатуре в виде двумерного массива! <em>Было</em> нельзя, до этого обновления. Теперь клавиатуры можно строить так (с помощью плагина grammy-markup):</p>
  <pre id="ABxg" data-lang="typescript">const keyboard = new InlineKeyboard([
  [IButton.text(&#x27;1&#x27;, &#x27;a&#x27;), IButton.text(&#x27;2&#x27;, &#x27;b&#x27;)],
  [IButton.text(&#x27;3&#x27;, &#x27;c&#x27;),
])</pre>
  <p id="XoUe">Плагин grammy-markup в связи с этим получает версию 1.0! <a href="https://github.com/Loskir/grammy-markup" target="_blank">Репо »</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@loskir/serverless</guid><link>https://teletype.in/@loskir/serverless?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir</link><comments>https://teletype.in/@loskir/serverless?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=loskir#comments</comments><dc:creator>loskir</dc:creator><title>Serverless</title><pubDate>Fri, 27 May 2022 19:11:12 GMT</pubDate><description><![CDATA[Это такой способ хостинга, при котором у разработчика нет контроля за серверами. Вместо этого программа загружается напрямую в serverless платформу, и уже она берет на себя всё остальное.]]></description><content:encoded><![CDATA[
  <h2 id="dqiJ">Что такое Serverless?</h2>
  <p id="DxmA">Это такой способ хостинга, при котором у разработчика нет контроля за серверами. Вместо этого программа загружается напрямую в serverless платформу, и уже она берет на себя всё остальное.</p>
  <p id="NaNi">Получается что-то типа цифрового коммунизма: каждому по потребностям, с каждого по затратам. Программы на Serverless платформах автоматически масштабируются, при резком наплыве запроосов получая дополнительные мощности, а платить приходится только за непосредственно потраченные ресурсы. Кроме того, у провайдеров зачастую есть бесплатные тарифы, что делает их привлекательным вариантом для маленьких хобби-проектов.</p>
  <p id="EE2G">Обычно serverless-провайдеры не разрешают держать код постоянно запущенным. Вместо этого взаимодействие с программой обычно строится на базе отдельных запросов. Чаще всего это обычные HTTP-запросы, но можно встретить возможность запуска кода по таймеру или по MQTT.</p>
  <h2 id="IpTR">Как работает Serverless?</h2>
  <p id="hCRo">Serverless-платформы можно поделить на два типа:</p>
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="4h33">Первый тип, традиционный, работает на базе контейнеров или виртуальных машин. Для пользовательского кода выделяется один или несколько инстансов с выделенными ресурсами (обычно около 128MB оперативки и какая-нибудь доля ядра в каждом). Если запросов приходит слишком много, поднимаются дополнительные инстансы. Если запросов приходит слишком мало, лишние инстансы убиваются. Разработчик платит за общее время жизни инстансов. <br />Так работают <strong>AWS Lambda</strong>, <strong>GCP</strong>, <strong>Heroku</strong> и ещё много других.</p>
  </section>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="BnxA">Второй же, появившийся совсем недавно, вместо изоляции операционной системы целиком, изолирует только javascript-песочницу внутри <strong>V8</strong>. Таким образом, масштабирование происходит быстрее (на запуск кода с нуля уходит 5-30мс вместо 200-500мс), а ресурсы можно распределять более оптимально (чаще всего деньги берут только за количество обработанных запросов, а на сами запросы ставят только лимит в несколько десятков миллисекунд чистого процессорного времени, без учёта io-bound задач). <br />Так работают <a href="https://workers.cloudflare.com/" target="_blank"><strong>Cloudflare Workers</strong></a> и <a href="https://deno.com/deploy" target="_blank"><strong>Deno Deploy</strong></a>. </p>
    <blockquote id="ZmRx">Отмечу, что эти провайдеры поддерживают только браузерный javascript (CF Workers) или Deno (Deno Deploy). Nodejs не подходит для такого из-за своей внутренней реализации (видимо, слишком слабая изоляция).</blockquote>
  </section>
  <h2 id="SXJy">Мой опыт</h2>
  <p id="afSf">Я попробовал много разных serverless-платформ (табличку со сравнением которых мы с сообществом grammY пытаемся вести <a href="https://grammy.dev/hosting/comparison.html" target="_blank">тут</a>). Некоторые мне понравились, а некоторые — не очень. Рассматривал я их больше с точки зрения бесплатного хостинга для хобби-проектов, хотя для средненагруженных проектов должно быть тоже очень интересно.</p>
  <p id="0Lp9">Самая большая проблема — это долговременное хранение данных. У serverless инстансов нет традиционной файловой системы, поэтому даже хранение сессий в телеграм-ботах становится проблемой. У некоторых провайдеров есть свои решения (например, <strong>Deta Base</strong>), но не у всех. grammY <a href="https://grammy.dev/plugins/session.html#free-storage" target="_blank">предоставляет</a> своё бесплатное хранилище (на базе S3), но оно подходит только для прототипов, потому что производительность у него очень слабая.</p>
  <p id="kBMF">Зато если бот или веб-сервер не имеют собственного состояния, serverless показывает себя крайне хорошо. Например, я за несколько минут переписал <a href="https://t.me/gmojibot" target="_blank">@gmojibot</a> на Deno (благо grammY его поддерживает) и захостил на <strong>Deno Deploy</strong>. Работает просто замечательно.</p>
  <h3 id="vPm9">Что мне не понравилось</h3>
  <p id="XLVC">А не понравились мне решения от <a href="https://scaleway.com" target="_blank">Scaleway</a>: Serverless Functions и Serverless Containers. А я надеялся, что Serverless Containers будут вести себя как обычные VPS-ки, только скейлиться при необходимости и оплачиваться по использованным ресурсам. А оказались обычными &quot;традиционными&quot; serverless решениями с долгими холодными стартами, не очень понятными ценовыми политиками и без доступа к файловой системе. Монгу в таком не захостишь, а так хотелось.</p>

]]></content:encoded></item></channel></rss>