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

</content></entry><entry><id>loskir:type-level-addition</id><link rel="alternate" type="text/html" href="https://teletype.in/@loskir/type-level-addition?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=loskir"></link><title>Складываем числа в системе типов Typescript</title><published>2022-11-13T13:20:28.637Z</published><updated>2022-11-13T18:28:49.773Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/5c/0e/5c0e4e62-6e01-4d33-957e-3b602bb774f5.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/48/ba/48ba17b3-e664-44b3-8928-1fe93ed4efef.png&quot;&gt;Недавно я участвовал в соревновании Yandex Cup, где одним из заданий в треке фронтенд-разработки было решить некую задачку полностью в рамках системы типов Typescript. Я очень люблю тайпскрипт в целом и его систему типов в частности, поэтому на это задание я потратил значительную часть всего времени. Что там было — совсем другая история, но именно тогда мне пришла идея написать свой сумматор на типах.</summary><content type="html">
  &lt;figure id=&quot;Pwml&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/48/ba/48ba17b3-e664-44b3-8928-1fe93ed4efef.png&quot; width=&quot;510&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;8Sgg&quot;&gt;Недавно я участвовал в соревновании &lt;a href=&quot;https://yandex.ru/cup&quot; target=&quot;_blank&quot;&gt;Yandex Cup&lt;/a&gt;, где одним из заданий в треке фронтенд-разработки было решить некую задачку полностью в рамках системы типов Typescript. Я очень люблю тайпскрипт в целом и его систему типов в частности, поэтому на это задание я потратил значительную часть всего времени. Что там было — совсем другая история, но именно тогда мне пришла идея написать свой сумматор на типах.&lt;/p&gt;
  &lt;p id=&quot;SVSc&quot;&gt;&lt;/p&gt;
  &lt;section style=&quot;background-color:hsl(hsl(0,   0%,  var(--autocolor-background-lightness, 95%)), 85%, 85%);&quot;&gt;
    &lt;p id=&quot;tQOq&quot;&gt;В процессе решения задания я немного поискал в интернете и наткнулся на проект &lt;a href=&quot;https://github.com/dqn/calc-ts&quot; target=&quot;_blank&quot;&gt;calc-ts&lt;/a&gt;, который реализует основные арифметические операции и парсинг выражений. Но у него обнаружился один фатальный недостаток. В силу особенностей реализации, в которые я не буду вдаваться, он перестаёт работать на числах больше нескольких тысяч. Там используется довольно наивный подход &amp;quot;давайте увеличивать одно число на единичку, а второе уменьшать, пока оно не станет равно нулю&amp;quot;, и тайпскрипт захлёбывается на таком количестве итераций.&lt;/p&gt;
    &lt;figure id=&quot;RSky&quot; class=&quot;m_retina&quot;&gt;
      &lt;img src=&quot;https://img2.teletype.in/files/5f/81/5f81bc19-b9a1-4447-bf81-acbbfa8e3c3d.png&quot; width=&quot;502&quot; /&gt;
    &lt;/figure&gt;
  &lt;/section&gt;
  &lt;p id=&quot;vNfS&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;jNKt&quot;&gt;Вместо этого я решил взять за основу десятичную систему и алгоритм сложения столбиком. Что ж, начнём! &lt;/p&gt;
  &lt;p id=&quot;9XXa&quot;&gt;Нам нужно реализовать тип, который принимает на вход два числа, а на выход выдаёт ещё одно число — сумму.&lt;/p&gt;
  &lt;p id=&quot;M6hx&quot;&gt;Сначала нужно превратить числа в строки, а строки разбить на отдельные цифры. Это, пожалуй, единственный способ удобной работы с числами в TS, ведь никаких арифметических операций в системе типов нет.&lt;/p&gt;
  &lt;pre id=&quot;rA2g&quot; data-lang=&quot;typescript&quot;&gt;type StringToChars&amp;lt;T extends string&amp;gt; = T extends &amp;#x60;${infer C}${infer Rest}&amp;#x60;
  ? [C, ...StringToChars&amp;lt;Rest&amp;gt;]
  : [];
type NumToChars&amp;lt;T extends number&amp;gt; = StringToChars&amp;lt;&amp;#x60;${T}&amp;#x60;&amp;gt;;&lt;/pre&gt;
  &lt;p id=&quot;MiVB&quot;&gt;Для преобразования числа в строку мы используем подстановку в template literal. А вот для разбиения строки на символы пригодится рекурсия и немного infer-магии. Простыми словами можно описать так:&lt;/p&gt;
  &lt;ul id=&quot;O3mo&quot;&gt;
    &lt;li id=&quot;WVaB&quot;&gt;на каждом шаге мы проверяем, представляет ли строка из себя нечто формата &lt;code&gt;&amp;#x60;${первый символ}${всё остальное}&amp;#x60;&lt;/code&gt; (причём всё остальное может быть пустой строкой)&lt;/li&gt;
    &lt;li id=&quot;mafR&quot;&gt;если да, то возвращаем первый символ и результат разбиения оставшейся строки (которая возвращает второй символ и результат разбиения оставшейся строки и так далее)&lt;/li&gt;
    &lt;li id=&quot;72Hi&quot;&gt;если нет, то возвращаем пустой массив (это нужно для завершения рекурсии, когда строка пустая)&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;GnmL&quot;&gt;Теперь из числа &lt;code&gt;12345&lt;/code&gt;  мы можем получить кортеж цифр &lt;code&gt;[&amp;quot;1&amp;quot;, &amp;quot;2&amp;quot;, &amp;quot;3&amp;quot;, &amp;quot;4&amp;quot;, &amp;quot;5&amp;quot;]&lt;/code&gt;. Дальше вспоминаем алгоритм сложения столбиком: числа нужно записать так, чтобы одинаковые разряды оказались друг над другом. В нашем случае — чтобы одинаковые разряды была на одинаковых позициях в кортеже. Как бы сделать так, чтобы цифра 5 в числе 12345 была на том же месте, что и 8 в 678? Развернуть массив!&lt;/p&gt;
  &lt;p id=&quot;hBzR&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;kaEy&quot;&gt;Для этого опять используем infer и рекурсию:&lt;/p&gt;
  &lt;pre id=&quot;gggq&quot; data-lang=&quot;typescript&quot;&gt;type Reverse&amp;lt;T extends any[]&amp;gt; = T extends [infer V, ...infer Rest]
  ? [...Reverse&amp;lt;Rest&amp;gt;, V]
  : [];&lt;/pre&gt;
  &lt;p id=&quot;TYLD&quot;&gt;&lt;code&gt;Reverse&amp;lt;NumToChars&amp;lt;12345&amp;gt;&amp;gt;&lt;/code&gt; даст нам &lt;code&gt;[&amp;quot;5&amp;quot;, &amp;quot;4&amp;quot;, &amp;quot;3&amp;quot;, &amp;quot;2&amp;quot;, &amp;quot;1&amp;quot;]&lt;/code&gt;, а &lt;code&gt;Reverse&amp;lt;NumToChars&amp;lt;678&amp;gt;&amp;gt;&lt;/code&gt; — &lt;code&gt;[&amp;quot;8&amp;quot;, &amp;quot;7&amp;quot;, &amp;quot;6&amp;quot;]&lt;/code&gt;. Как видите, разряды действительно совпадают.&lt;/p&gt;
  &lt;p id=&quot;8N1U&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;IgoZ&quot;&gt;Теперь нужно научиться складывать две цифры. Для этого я написал маленький скриптик, который генерирует look-up table:&lt;/p&gt;
  &lt;pre id=&quot;zTbE&quot; data-lang=&quot;javascript&quot;&gt;for (let i = 0; i &amp;lt;= 9; ++i) {
  console.log(&amp;#x60;A extends &amp;#x27;${i}&amp;#x27; ? (&amp;#x60;)
  for (let j = 0; j &amp;lt;= 9; ++j) {
    console.log(&amp;#x60;B extends &amp;#x27;${j}&amp;#x27; ? [${i + j &amp;gt;= 10 ? &amp;#x60;&amp;#x27;${(i+j) % 10}&amp;#x27;, true&amp;#x60;: &amp;#x60;&amp;#x27;${i+j}&amp;#x27;, false&amp;#x60;}] :&amp;#x60;)
  }
  console.log(&amp;#x27;never) :&amp;#x27;)
}
console.log(&amp;#x27;never&amp;#x27;)&lt;/pre&gt;
  &lt;p id=&quot;AmIb&quot;&gt;Результат выглядит примерно так:&lt;/p&gt;
  &lt;pre id=&quot;TLBj&quot; data-lang=&quot;typescript&quot;&gt;type AddDigitsCore&amp;lt;A extends string, B extends string&amp;gt; = A extends &amp;quot;0&amp;quot; ? (
    B extends &amp;quot;0&amp;quot; ? [&amp;quot;0&amp;quot;, false]
      : B extends &amp;quot;1&amp;quot; ? [&amp;quot;1&amp;quot;, false]
      : B extends &amp;quot;2&amp;quot; ? [&amp;quot;2&amp;quot;, false]
      // тут ещё 100 строчек…&lt;/pre&gt;
  &lt;p id=&quot;Scab&quot;&gt;Тип принимает на вход два символа и на выход отдаёт символ, который нужно записать в ответ, а также нужно ли добавить единичку к следующему разряду.&lt;/p&gt;
  &lt;p id=&quot;jv2I&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;yws6&quot;&gt;Как добавлять единичку? Тут мне пришлось написать маленький костыль, потому что переделывать по-нормальному мне было лень.&lt;/p&gt;
  &lt;pre id=&quot;rE0t&quot; data-lang=&quot;typescript&quot;&gt;type AddOne&amp;lt;T extends [string, boolean]&amp;gt; = [
  AddDigitsCore&amp;lt;T[0], &amp;quot;1&amp;quot;&amp;gt;[0],
  AddDigitsCore&amp;lt;T[0], &amp;quot;1&amp;quot;&amp;gt;[1] extends true ? true : T[1],
];&lt;/pre&gt;
  &lt;p id=&quot;1DLw&quot;&gt;Тип принимает на вход кортеж из символа и индикатора переполнения и отдаёт такой же кортеж на выход. Символ — это результат прибавления единички к входному символу. А вот для индикатора переполнения написано условие. Если он равен true, то возвращаем true. Иначе возвращаем то, что было передано на вход. Выглядит не очень красиво, но работает, и вот почему:&lt;/p&gt;
  &lt;ol id=&quot;nLTP&quot;&gt;
    &lt;li id=&quot;3o1e&quot;&gt;Если на вход передаётся число с переполнением (например 12, то есть &lt;code&gt;[&amp;quot;2&amp;quot;, true]&lt;/code&gt;), то на выходе переполнение остаётся (получается 13, то есть &lt;code&gt;[&amp;quot;3&amp;quot;, true]&lt;/code&gt;)&lt;/li&gt;
    &lt;li id=&quot;UpXm&quot;&gt;Переполнения переполнения случиться не может, потому что максимальное число на входе — это 9+9, то есть 18&lt;/li&gt;
    &lt;li id=&quot;exmK&quot;&gt;Если на вход передаётся число от 0 до 8, то переполнения не появляется&lt;/li&gt;
    &lt;li id=&quot;9cNq&quot;&gt;Единственный случай, когда появляется переполнение — когда передаётся 9 (&lt;code&gt;[&amp;quot;9&amp;quot;, false]&lt;/code&gt; =&amp;gt; &lt;code&gt;[&amp;quot;0&amp;quot;, true]&lt;/code&gt;) &lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;kgDP&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;gypC&quot;&gt;Теперь напишем тип для сложения двух цифр, но с учётом возможной «единички»:&lt;/p&gt;
  &lt;pre id=&quot;iisg&quot; data-lang=&quot;typescript&quot;&gt;type AddDigits&amp;lt;
  A extends string,
  B extends string,
  Leftover extends boolean = false,
&amp;gt; = Leftover extends true ? AddOne&amp;lt;AddDigitsCore&amp;lt;A, B&amp;gt;&amp;gt; : AddDigitsCore&amp;lt;A, B&amp;gt;;&lt;/pre&gt;
  &lt;p id=&quot;47Vz&quot;&gt;Тут всё просто: если Leftover = true, то складываем цифры и добавляем единичку; если нет, то просто складываем цифры.&lt;/p&gt;
  &lt;p id=&quot;Yb5W&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;Cv7C&quot;&gt;Дальше напишем ещё несколько типов, которые пригодятся нам позже. &lt;br /&gt;Первый — это тип Shift, который удаляет первый элемент из кортежа.&lt;/p&gt;
  &lt;pre id=&quot;1OWF&quot; data-lang=&quot;typescript&quot;&gt;type Shift&amp;lt;T extends any[]&amp;gt; = ((...v: T) =&amp;gt; void) extends
  ((v: any, ...rest: infer V) =&amp;gt; void) ? V : [];&lt;/pre&gt;
  &lt;p id=&quot;qk5F&quot;&gt;Я не очень понимаю, зачем тут используются сигнатуры функций, но без них что-то ломается. В общем, тип «проглатывает» первый элемент, а все остальные возвращает обратно. Если массив и так пустой, то просто возвращает пустой массив.&lt;/p&gt;
  &lt;p id=&quot;Xi0e&quot;&gt;Второй — это тип FallbackToZero, тут всё очевидно.&lt;/p&gt;
  &lt;pre id=&quot;HfK4&quot; data-lang=&quot;typescript&quot;&gt;type FallbackToZero&amp;lt;T extends string | undefined&amp;gt; = T extends undefined ? &amp;quot;0&amp;quot; : T;&lt;/pre&gt;
  &lt;p id=&quot;xl6q&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;N4kS&quot;&gt;Теперь подумаем о том, как бы нам реализовать алгоритм сложения. Шаги должны быть такие:&lt;/p&gt;
  &lt;ol id=&quot;cU3e&quot;&gt;
    &lt;li id=&quot;OMwO&quot;&gt;Сложить два числа в текущем разряде (не забыть про возможную «единичку» сверху)&lt;/li&gt;
    &lt;li id=&quot;TGq7&quot;&gt;Записать последнюю цифру результата в ответ&lt;/li&gt;
    &lt;li id=&quot;xZI5&quot;&gt;Запомнить «единичку» (если она есть) для следующего разряда&lt;/li&gt;
    &lt;li id=&quot;iZIW&quot;&gt;Перейти к следующему разряду&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;kqjf&quot;&gt;В качестве решения напрашивается рекурсия, её и будем использовать. Получился тип &lt;/p&gt;
  &lt;pre id=&quot;AasR&quot; data-lang=&quot;typescript&quot;&gt;type AddCore&amp;lt;
  A extends string[],
  B extends string[],
  Leftover extends boolean = false,
&amp;gt; = true extends (
  &amp;amp; (A[0] extends undefined ? true : false)
  &amp;amp; (B[0] extends undefined ? true : false)
) ? Leftover extends true ? &amp;quot;1&amp;quot; : &amp;quot;&amp;quot;
  : &amp;#x60;${AddCore&amp;lt;
    Shift&amp;lt;A&amp;gt;,
    Shift&amp;lt;B&amp;gt;,
    AddDigits&amp;lt;
      FallbackToZero&amp;lt;A[0]&amp;gt;,
      FallbackToZero&amp;lt;B[0]&amp;gt;,
      Leftover
    &amp;gt;[1]
  &amp;gt;}${AddDigits&amp;lt;
    FallbackToZero&amp;lt;A[0]&amp;gt;,
    FallbackToZero&amp;lt;B[0]&amp;gt;,
    Leftover
  &amp;gt;[0]}&amp;#x60;;&lt;/pre&gt;
  &lt;p id=&quot;3qUl&quot;&gt;Давайте разбираться, что тут происходит. На вход приходит два числа (в виде массива символов в обратном порядке) и индикатор «единички». Для первого разряда он всегда false, но для следующих итераций пригодится.&lt;/p&gt;
  &lt;pre id=&quot;Imx0&quot; data-lang=&quot;typescript&quot;&gt;AddDigits&amp;lt;
  FallbackToZero&amp;lt;A[0]&amp;gt;,
  FallbackToZero&amp;lt;B[0]&amp;gt;,
  Leftover
&amp;gt;[0]&lt;/pre&gt;
  &lt;p id=&quot;46Xd&quot;&gt;Тут мы получаем цифру, которая идёт в ответ.&lt;/p&gt;
  &lt;pre id=&quot;tKso&quot; data-lang=&quot;typescript&quot;&gt;AddCore&amp;lt;
  Shift&amp;lt;A&amp;gt;,
  Shift&amp;lt;B&amp;gt;,
  AddDigits&amp;lt;
    FallbackToZero&amp;lt;A[0]&amp;gt;,
    FallbackToZero&amp;lt;B[0]&amp;gt;,
    Leftover
  &amp;gt;[1]
&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;b4KA&quot;&gt;Тут мы запускаем рекурсию для следующего разряда. В &lt;code&gt;Leftover&lt;/code&gt; передаётся индикатор переполнения из результата сложения текущего разряда. Из чисел убирается по одной цифре с помощью метода &lt;code&gt;Shift&lt;/code&gt;. С каждой итерацией остаётся всё меньше и меньше цифр. Если одно число короче другого, то в один момент у него закончатся цифры, и &lt;code&gt;A[0]&lt;/code&gt; (или &lt;code&gt;B[0]&lt;/code&gt;) вернёт &lt;code&gt;undefined&lt;/code&gt;. Тут пригождается &lt;code&gt;FallbackToZero&lt;/code&gt;, который заменяет &lt;code&gt;undefined&lt;/code&gt; на &lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;saux&quot; data-lang=&quot;typescript&quot;&gt;true extends (
  &amp;amp; (A[0] extends undefined ? true : false)
  &amp;amp; (B[0] extends undefined ? true : false)
) ? Leftover extends true ? &amp;quot;1&amp;quot; : &amp;quot;&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;HLA3&quot;&gt;Тут мы проверяем, не закончились ли у нас разряды у обоих чисел сразу. Если так, то мы завершаем рекурсию и возвращаем 1, если у нас осталась «единичка», либо пустую строку.&lt;/p&gt;
  &lt;p id=&quot;y6xm&quot;&gt;В результате у нас получается сумма чисел в виде строки. Никаких дополнительных манипуляций с ней производить не нужно, она уже записана в правильном порядке (старший разряд слева). Можно было попытаться возвращать результат в виде числа, но я не смог придумать оптимальный способ конвертации символов в число.&lt;/p&gt;
  &lt;p id=&quot;JRMM&quot;&gt;Ну и наконец делаем юзер-френдли тип, который принимает на вход два числа, конвертирует их в обратный массив символов и вызывает &lt;code&gt;AddCore&lt;/code&gt;&lt;/p&gt;
  &lt;pre id=&quot;xsQm&quot; data-lang=&quot;typescript&quot;&gt;type Add&amp;lt;A extends number, B extends number&amp;gt; = AddCore&amp;lt;
  Reverse&amp;lt;NumToChars&amp;lt;A&amp;gt;&amp;gt;,
  Reverse&amp;lt;NumToChars&amp;lt;B&amp;gt;&amp;gt;
&amp;gt;;&lt;/pre&gt;
  &lt;p id=&quot;kWBQ&quot;&gt;Готово! Потыкать можно в &lt;a href=&quot;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&quot; target=&quot;_blank&quot;&gt;песочнице&lt;/a&gt; посмотреть код — на &lt;a href=&quot;https://github.com/Loskir/typescript-addition&quot; target=&quot;_blank&quot;&gt;гитхабе&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;dqyh&quot;&gt;Подписывайтесь на мой &lt;a href=&quot;https://t.me/loskirs&quot; target=&quot;_blank&quot;&gt;телеграм-канал&lt;/a&gt;, ставьте лайки, до связи!&lt;/p&gt;

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

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

</content></entry></feed>