Steven
Steven5 хв читання

Як транслювати звіт у реальному часі без мерехтіння

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

Інженерія
Фронтенд
Стрімінг
Релізи GeekBye
Як транслювати звіт у реальному часі без мерехтіння

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

GeekBye v1.6.13–v1.6.15 перетворив це на звіт, що вливається наживо, поле за полем, у міру генерації. Цікава не сама потокова передача — а все, що нам довелося зробити, щоб стрімінг не виглядав смикано. Спокійний стрімінг і сокирний стрімінг — це одна й та сама функція з дуже різним виконанням.

Мерехтіння, частина перша: не дай кожній порції трясти React

Підсумок — це не один блок, а структурні поля (загальна оцінка, ключові моменти, задачі і так далі), які бекенд видає по одному через server-sent events. Наївний спосіб це відрендерити — оновлювати стан React на кожній події в міру надходження.

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

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

// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })

// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>

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

Мерехтіння, частина друга: рендерер, який ми вже мали

Друге мерехтіння тонше й живе в самому markdown. Звіт рендерить багатий markdown — заголовки, жирний текст, списки, блоки коду. Але markdown, що приходить посеред потоку, у будь-яку дану мить недописаний. Список з одним пунктом поки що. Блок коду, який відкрили, але не закрили. Маркер жирного без пари.

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

Виправлення було майже ніяково доступним: ми вже використовували рендерер markdown з урахуванням стрімінгу для живого чату з ШІ. Рендерер, побудований терпіти неповні токени — рендерити недописаний markdown стабільно й усталювати його лише в міру дописування токенів — замість того щоб щоразу парсити з нуля. Звіту треба було просто використати той самий. Ми розв'язали рівно цю задачу для чату місяцями раніше; «виправленням» для звіту було усвідомлення, що інструмент у нас уже є, і скерування його на другу поверхню. Повторне використання побило перебудову.

Бонус: форматоване копіювання — це задача формату буфера обміну

Коли звіт став гарно виглядати, люди захотіли його вставляти — у документ, у лист — і зберігати форматування та скриншоти. Інстинкт — серіалізувати звіт у рядок markdown і сподіватися, що ціль його відрендерить. Зазвичай ні.

Справжня відповідь у тому, що буфер обміну тримає кілька представлень одразу. Тож ми записуємо два: HTML-версію зі збереженим форматуванням і скриншотами, вкладеними як inline-зображення, і текстовий запасний варіант для цілей, які не приймають HTML. Встав у форматований редактор — отримаєш відформатований звіт із картинками; встав у просте текстове поле — отримаєш чистий текст. «Форматоване копіювання» ніколи не було задачею серіалізації — це була задача обери-правильний-формат-буфера-обміну.

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

Три речі, яких нас навчив потоковий інтерфейс

  1. Буферизуй потокові поля, публікуй раз на порцію. Дати кожній server-sent event гнати власне оновлення стану — це і є мерехтіння. Акумулятор у ref плюс єдина публікація на порцію, у дерево, яке ніколи не демонтується, перетворює нервове збирання на спокійне заповнення.
  2. Усе, що стрімиться, потребує рендерера з урахуванням стрімінгу. Рендерер, що повторно парсить увесь рядок на кожному токені, мерехтітиме на недописаному markdown. Використовуй побудований під неповний ввід — а якщо в тебе вже є один для іншої поверхні, переви́користай його, перш ніж будувати другий.
  3. Форматоване копіювання — про формати буфера обміну, а не про серіалізацію. Запиши text/html із вкладеними зображеннями та запасний text/plain. Буфер обміну спроєктовано нести обидва; користуйся цим.

Це п'ятий розділ історії про надійність і оздоблення, що стає GeekBye v2. За попереднім розділом дивись баг із навушниками-вкладишами (v1.6.12); за тим, де та сама ідея server-sent events пізніше стала цілим запасним транспортом, транскрипція наживо, коли фаєрвол блокує WebSockets (v2.0.8); а за всією дугою — анатомію доведення софту до досконалості.

Схожі статті

Розрив з'єднання не повинен ронити весь застосунок — а наш ронив
Steven
Steven6 хв читання

Розрив з'єднання не повинен ронити весь застосунок — а наш ронив

Коли наш бекенд відвалився посеред зустрічі, він не просто призупинив транскрипцію — він поклав увесь застосунок. Причиною була одна необроблена подія, а виправлення вмістилося в десять рядків. Це кластер релізів, завдяки якому GeekBye лишається в системі й на зв'язку крізь те, що раніше його вбивало.

Інженерія
Надійність
Electron
Анатомія доведення ПЗ до досконалості: як код-рев'ю зловило те, що не змогли тести
Steven
Steven5 хв читання

Анатомія доведення ПЗ до досконалості: як код-рев'ю зловило те, що не змогли тести

Упродовж усієї серії GeekBye v2 повторюється те саме: виправлення проходить кожен тест на машині розробника, а потім код-рев'ю доводить, що воно б не спрацювало майже ні в кого. Ось робочий процес за дев'ятьма релізами — ворота рев'ю, відлови на етапі виправлення і дисципліна «протестуй до відвантаження», яка перетворює «у мене працює» на «працює».

Інженерія
Процес
Код-рев'ю
Ваш застосунок для Mac отримує доступ до мікрофона — і забуває його при кожному запуску
Steven
Steven5 хв читання

Ваш застосунок для Mac отримує доступ до мікрофона — і забуває його при кожному запуску

GeekBye попросив доступ до мікрофона, ви його надали, і все запрацювало. Наступний запуск: доступу немає. А в Системних параметрах → Мікрофон застосунок узагалі не з’явився. Винуватцем виявилася функція безпеки macOS, що тихо запускає застосунок зі шляху, який зникає — ось діагноз і виправлення в одне натискання.

macOS
Дозволи
Інженерія