Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

26.09.2023

Плагін для Obsidian - як вони випускаються

Мій плагін нарешті став корисним, а значить, настав час перенести його з тестової бази на справжню.

Всі плагіни, що в тебе встановлені, сидять в теці .obsidian/plugins всередині сховища. Це значить, що в кожного сховища плагіни свої. Тому для розробки плагіну розумно створити окреме сховище документів та встановити плагін туди — тоді можна спокійно та без ризиків все протестувати.

Плагіни також синхронізуються між пристроями та навіть підключаються в мобільну версію. Так що плагін для Obsidian - це, можливо, найпростіший спосіб створити мобільний додаток. Навіть місце для зберігання даних та синхронізація буде. Та й дійсно, мій плагін без додаткових зусиль запрацював на телефоні. Тепер я навіть планую особливі екрани для телефону, щоб нашвидкуруч робити прості організаційні дії.

Як тоді зробити релізну версію плагіну та встановити її? Виявилось, дуже просто. Пакет складається з трьох файлів: коду, стилів, та маніфесту. Причому так для всіх плагінів. Інші ресурси — наприклад, зображення — ховаються в файлі з кодом, якщо я правильно розумію. Принаймні великих CSS в готових плагінах я не побачив.

Щоб випустити плагін для всіх (що я поки зробити не готовий), треба зробити репозиторій на GitHub, створити в ньому нову версію та підʼєднати до неї ці три файли як це зроблено тут. Трохи дивний спосіб випускати пакети, але він працює та не потребує додатково інфраструктури. Каталог плагінів теж розташований на Гітхабі.

А для себе достатньо створити нову теку в .obsidian/plugins, скопіювати туди файли та увімкнути плагін в налаштуваннях — це все. В мобільній версії плагін зʼявиться сам (якщо увімкнена синхронізація, звісно.) Одним словом, хороше середовище для розробки, раджу спробувати.


25.09.2023

Логічна реплікація в PostgreSQL як засіб відстеження змін

Я вже писав про логічну реплікацію як найкраще рішення для часткової синхронізації двох баз PostgreSQL. Нова версія PostgreSQL 16, яка вийшла десять днів тому, містить ще декілька покращень для логічної реплікації. Я радий тому, що вона активно доробляється, оскільки, як писав, не все там ідеально.

А сьогодні про інше. Логічна реплікація дуже проста технологія — одна база зберігає журнал, інша забирає його по протоколу реплікації та відтворює зміни у себе. За цим протоколом не тільки PostgreSQL, а будь-яка програма може забирати зміни та робити з ними що завгодно.

Виходить, що з PostgreSQL можна зробити маленьку Кафку: замість топіка в нас буде журнал WAL, причому підписників як і в Кафки може бути декілька. З тою різницею, що PostgreSQL також зберігатиме дані в таблиці, як завжди. Точніше, у зворотному сенсі: до звичайних таблиць можна також додати сервіс, який оброблятиме зміни у формі подій. Може, для аналітики, може, для побудови похідного стану. Реплікація працює миттєво та не потребує сканування таблиці.

З технічної реалізації: ось пакет pglogrepl для Go. Я перевірив локально — все працює. Ми отримуємо зміни у вигляді структур — знати схему таблиці не обовʼязково. Налаштувати підключення до бази ледве складніше, ніж звичайне.

Може, спробую наступного разу, як доведеться будувати реактивну логіку навколо бази PostgreSQL.


24.09.2023

Думки про ремонт

В мене останні три дні було закінчення підготовки до ремонту. Було зроблено роботи на місяць, мабуть.


23.09.2023

Чому саме дерево — найкраща структура для задач?

Наступна фішка для моєї системи задач в Obsidian - то підтримка вкладеності. Але не просто вкладеності пунктів — то зрозуміло. Мені хочеться, щоб вкладеність працювала через документи.

Тобто якщо задача має вигляд - [ ] [[ремонт]], то документ ремонт з усім своїм змістом вважається частиною дерева задач. Це дозволяє досягнути довільного рівня вкладеності без створення непомірно довгих списків.

Мій поточний досвід показує, що при плануванні це допомагає. Реальні задачі мають категорично різні рівні складності, та загнати відразу все в красиво обмежені абстракції не виходить.

Точніше, не так: “картки” в Trello, або “епіки/задачі” в Jira зручні для виконання вже після того, як планування закінчене та ми маємо чіткий набір кроків. Хоч всім знайомо, що деякі картки виявляються складними та займають багато днів, а інші, що стоять поруч, виконуються за пʼять хвилин.

От тільки в мене ніколи не буває окремих етапів планування та виконання. Та завжди може зʼявитися потреба розбити один з пунктів ще глибше. Тому прийшов до того, що дерево задач — найбільш природна та зручна форма. Особливо коли це дерево зберігається в Markdown та легко може доповнюватись нотатками, посиланнями, ілюстраціями й так далі.

Проте видобувати з дерева потрібні пункти (наприклад, те, що я зараз роблю, або всі термінові задачі) вручну не є практичним. А коли йдеться про вкладені документи, то й зовсім нереальним. Тому будувати з дерева списки будуть мої віджети.


22.09.2023

Кілька порад з балансу роботи та життя

Попросили розказати. Я як людина що вже 13 років на віддаленій роботі, відчуваю себе дуже кваліфікованим. :) Принаймні, щоб розуміти розмір проблеми, бо сказати, що в мене той баланс ідеальний, ніяк не можу. Проте тепер всі на віддаленці, та напевно, мають ті самі проблеми.

З хорошого: гнучкий графік та віддалена робота надають можливість працювати в найкращі години. У кожного вони свої, в тому й справа. Я полюбляю працювати рано вранці (хоча з роками шоста ранку перетворилась на восьму.) А також мати можливість підстроїти власний графік під сімʼю.

З поганого: такий режим хоч і є ключем до чудового балансу, але й вішає на тебе відповідальність. Варто це усвідомлювати. Робота вдома не має ніякого явного кінця, тому закінчувати її потрібно свідомо. Або ніякого балансу не буде.

…Я довгий час думав що, якщо трохи переривати роботу, а потім трохи доробляти “в неробочий час”, то це й буде балансом. Але ж так не виходить тому, що є нюанс — я люблю свою роботу. Та в виборі: робота або спілкування, прибирання, і так далі — робота має фору. Власне, це і є першопричиною поганого робоче-життєвого балансу. Що з цим робити? Я маю декілька порад:

Виходить, для гарного балансу доведеться власноруч відтворити частину аспектів роботи в офісі. Не все там погано. (Можеш ще й ласощів на кухню закупити.)


21.09.2023

Просто почни з чогось (себто, с плану)

Сьогодні почув про випадок, коли людина застрягла на простій задачі цілий день, бо не вистачало якоїсь інформації. Я цьому дуже співчуваю, бо в самого постійно виходить. В такому випадку відсутність ясності веде до ментального заціпеніння та міркувань по колу, з якого немає виходу. Про мікровідволікання я писав, а це, можна сказати, “макровідволікання”.

В ідеалі, це саме такий випадок, в якому має спрацювати чуйка на проблеми. Нехай задачу ми розвʼязати відверто не можемо. Розв’яжімо метазадачу, а саме, що робити з ходінням по колу.

Припустимо, що перемикнутися на іншу задачу ми не можемо. Може, наша нерозвʼязна задача супер важлива — або іншої наразі просто немає. Потрібно просуватись з цією.

Я б радив в такій ситуації почати з того, щоб відкрити порожній файл та записати все, що приходить в голову — питання, плани, рішення. Дилеми.

Далі, в більшості випадків для мене, магічним образом зʼявляється шлях вперед. В дилемі знаходиться компроміс — хоч би тимчасовий. Питання — розвʼязуються або відкладаються на пізніше. Головне, що замість міркування по колу я можу зробити крок до розвʼязку… та зазвичай цього достатньо, щоб розблокувати решту шляху.

Проте все ж на мою думку головною перешкодою є нездатність помітити, що ти зациклився. Якби на це вистачало самосвідомості, то все стає простіше.


20.09.2023

Інтеграція Svelte у плагін Obsidian

Дійшов до реалізації свого віджету для Obsidian. Як я писав, його нескладно інтегрувати через registerMarkdownCodeBlockProcessor. Ця функція надає нам елемент HTML, в якому можна розташувати майже що завгодно.

Проте зовсім не хочеться збирати віджет з HTML вручну. Має сенс залучити фреймворк. Останній рік Svelte - мій вибір для простих задач. Хоча в принципі, в Obsidian можна інтегрувати будь-який фреймворк — бо на радість або на смуток, Obsidian цілком побудований на браузері (Electron), та дає такий само елемент-контейнер, який ми мали б в каркасі звичайного додатка.

Офіційна інструкція містить все, що потрібно щоб почати. Svelte абсолютно без проблем інтегрується в ESBuild (бо офіціально плагіни для Obsidian збираються саме ним.) Мені не зовсім зрозуміло, чому інструкція використовує store для передачі в компоненти плагіну; обʼєкт плагіну, наскільки я знаю, ніколи не змінюється.

Інструкція не розповідає про те, як додати компонент Svelte в документ, як я це хочу — але це робиться очевидно. В обробнику блока створюємо компонент та передаємо йому наш кореневий документ. Готово. Ну, майже — ще добре було б видаляти компонент, коли він більше не потрібний, бо інакше, якщо я розумію правильно, буде текти памʼять. Для цього знайшов метод MarkdownPostProcessorContext.addChild(). Він дозволяє зареєструвати обробник на видалення блоку. В теорії, можна було б в цьому MarkdownRenderChild інкапсулювати весь компонент, але мені поки простіше зробити клас з назвою SvelteDisposer, який просто видаляє вказаний компонент при видаленні блоку.

Тепер, нарешті, можна робити всілякі цікаві віджети та звіти по задачах.


19.09.2023

Перевірка на наявність поля в TypeScript: від поганого до кращого

В продовження обмежень TypeScript. Припустимо маємо такий собі interface ListItem { taskDates?: { due: string } }. Якщо цей пункт списку — задача, то він має дати. Якщо ні — то не має. Ми пишемо код, який щось робить з цими датами (наприклад, обирає задачі зі строком на сьогодні.)

Криво: Оператор невпевненості, або як його справді називають, “безпечної навігації”.

if (listItem.taskDates?.due === today) { ... }

Цей код нічого не каже про сенс наявності taskDates. Він просто припускає, що їх може не бути. Він семантично порожній. Хоч такий код і працює, але його читач залишається здогадуватись про зміст самотужки. Потім проєктом розповсюджуються захисні конструкції, а з ними — невпевненість. Є значення? Немає? Перевірю на всяк випадок, хто його зна.

Чітко: Перевіряти на наявність поля на початку функції.

if (!listItem.taskDates) {
  return; // або навіть throw
}
if (listItem.taskDates.due === today) { ... }

Тут навпаки ми явно кажемо: якщо дат немає — то ця функція втрачає свій сенс. В залежності від контексту, можна або просто вертати, або кидати помилку (тоді зовнішній код зобовʼязується викликати функцію тільки коли дати є). Читач розуміє, що код розрахований саме на елементи, в яких є taskDates.

Майстерно: обмежувати тип так, щоб виклик потребував чіткого набору полів.

interface TaskListItem {
  taskDates: { due: string };
}
// або з utility-types:
type TaskListItem = Required<ListItem, "taskDates">;

Оскільки в TypeScript типи порівнюються за змістом, то коду, який викликає нашу функцію, не треба утворювати новий обʼєкт. Достатньо тільки перевірити, що поле присутнє, та обʼєкт типу ListItem задовольнить тип TaskListItem. Тобто десь все одно буде перевірка. Різниця тільки в тому, що краще розмістити її якнайвище в алгоритмі, та в такому місці, де конкретизація до пунктів-задач буде очікуваною: ззовні у нас всілякі пункти, а всередині — тільки задачі.

Як бачите, різниця тут не в правильності (всі варіанти однаково правильні), а в наочності в поясненні людям. В цьому треба спиратися на систему типів, а не сперечатися з нею.


18.09.2023

Не все так просто з Кафкою: стан

В продовження теми про вивантаження всього змісту з Кафки. При перевірці виявилося що обставини трохи складніше, ніж я думав.

Що треба зрозуміти — в семантиці Kafka дані течуть, як річка, та стан цієї річки складно вловлюється. Та друге — топіки розділяються на розділи. Кожний розділ — це окремий потік даних. Ми обовʼязково маємо переконатись, що кожний розділ оброблений. Дані з розділів надходять в невизначеному порядку. Навіть якщо ми ще не отримували з деякого розділу жодного запису, це не значить, що їх там немає.

Ключем до розвʼязку є зміщення даних в розділі. А саме. зміщення останнього запису з кожного розділу — доступне через API ListOffset. Залишається один раз зберігти поточне значення, а потім споживати, аж допоки не дійдеш до зміщення, яке записав на початку операції. Далі — вже тільки нові записи.

От тільки якщо в одному з розділів неспожитих даних немає, то доведеться чекати нових, щоб зрозуміти, що старих немає — та, можливо, взагалі не дочекатись. Щоб цього уникнути, потрібно також перевіряти зміщення, яке вже спожила наша група споживання — через API Offset Fetch. (Такі чудові назви API, не переплутаєш.) Якщо спожите зміщення дорівнює кінцевому, то на цей розділ можна не дивитись.

Розроблений таким чином алгоритм реально працює. Попри меншу передбачуваність, ніж я звик, все ж можна написати тести, які наповнюють топік, а потім з нього читають. Та це чудово, бо перевіряти таке вручну було б проблематично.


17.09.2023

Автосадівник для задач в Obsidian- нарешті працює

Радий поділитися, що вчорашні зміни в підході були плідними, та решта утілити була дописана швидко. Тепер в мене є плагін для Obsidian, який автоматично дописує дати (початку та кінця) до задач, а також пересуває задачі, які було закінчено раніше, в спеціальну “архівну” зону в кінці документа.

Все це працює завдяки прямому редагуванню функцією Editor.replaceRange. Позиції для змін я беру з синтаксичного дерева, яке робить Remark. Є тільки один трюк — щоб ці позиції залишалися вірними, всі зміни маю робити з кінця (бо при зміні документа псуються тільки ті позиції, які знаходяться після неї.)

Також допомогло те, що додавання дат обмежується одним рядком, тобто вертикальна позиція зберігається. Це добре, бо далі потрібно пересувати задачі до архіву. Тут синтаксично проста операція, бо в Markdown елементи списку завжди починаються з нового рядка. Тобто щоб пересунути елемент з усім його змістом на нове місце, можна просто вирізати та вставляти цілі рядки. (Так, до речі, моє рішення чудово працює з вкладеними задачами, проте до архіву пересуваються тільки завершені задачі верхнього рівня.)

Заголовок архіву теж додаю банально текстом, без всіляких синтаксичних дерев. А от перевіряю, що заголовок є, по дереву — бо дерево прибирає всі нюанси форматування та перевірка стає простіше.

Нарешті, всю обробку я переніс в подію Workspace.editor-change, яка відбувається миттєво при будь-яких змінах — бо Vault.modify відбувається при зберіганні файлу, тобто з затримкою. Тоді й зміст документа можна брати прямо з Editor.getValue, який є синхронною операцією (бо з Vault.read є ризик відстати від актуального стану). Виходить, вся логіка відбувається в контексті редактора, а не файлу, що… логічно. А щоб не обробляти буквально кожне натиснення на клавішу, використовую старий знайомий debounce.