Месяц назад я написал статью, объясняющую стек и цепочку инструментов, которые я собрал для создания крошечных адаптивных веб-приложений.

Выбранная мной технология решила следующие проблемы:

  • Я хочу платить только за то, что использую (масштабирование до нуля)
  • У меня не так много времени для обучения или создания (нет крутых кривых обучения без существенной экономии времени)
  • У меня нет времени на техническое обслуживание (нет исправлений серверов, автоматизированное масштабирование)
  • Я не хороший UI-дизайнер или фронтенд-инженер (дизайн-системы — это здорово).

Мой стек работает очень хорошо для приложения со стандартными операциями создания/получения/обновления/удаления, но недавно я столкнулся с требованием, требующим большей сложности на серверной части.

Проблема бизнеса

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

  • Неуправляемая медитация на время
  • Возможность продолжить, когда таймер закончился, если хотите
  • Возможность приостановить и возобновить сеанс
  • Успокаивающий окружающий фоновый звук
  • Периодический звонок, чтобы проверить, что я все еще в данный момент
  • Случайные прерывистые, но актуальные звуки
  • Автоматический дневник сеансов

Новое требование

Мое приложение работает очень хорошо, но я решил, что пришло время добавить некоторые статистические данные, чтобы обеспечить себе дополнительную поддержку. Обычная статистика мотивации для приложений для здоровья и фитнеса — показать самую длинную «полосу», с которой справился пользователь. В случае с приложением для медитации статистикой является максимальное количество дней подряд, в течение которых пользователь медитировал.

Мой текущий набор инструментов выглядит так:

Это двухуровневая архитектура, поэтому вся бизнес-логика находится на стороне клиента в браузере. Это нормально для текущих требований, но все меняется, когда мне нужно вычислить статистику. Я мог бы придерживаться этого стека и подсчитывать серии сессий в своем дневнике на стороне клиента, но сейчас я приближаюсь к 200 сессиям в моем дневнике. Если я перенесу это на пару лет вперед, то для отображения моей страницы статистики мне придется извлекать много данных из базы данных Firebase Realtime каждый раз, когда она загружается, и без необходимости повторять вычисления в браузере. Если однажды у меня будет много пользователей, то будет много данных, которые будут извлекаться без необходимости много времени, а это стоит денег.

Триггеры Firebase

Firebase удивительно богат функциональностью. Я объяснил, как я использовал функции Firebase (функции облачной платформы Google) в другой истории для создания REST API. Функции Firebase включают возможность регистрировать функции в качестве триггеров базы данных. Вместо запуска функции в ответ на HTTP-вызов функция может запускаться при создании, обновлении или удалении данных в базе данных. Код запускается с правами администратора и имеет доступ к запустившему его субъекту пользователя, а также к моментальному снимку данных, поэтому он идеально подходит для поддержания таблиц статистики в актуальном состоянии в ответ на изменения данных.

Я следовал примеру, используя триггер onUpdate(), подробно описанный в документации. Я написал функцию, чтобы определить самую длинную полосу из списка записей дневника, и ее подключение было просто вопросом захвата записей из поля after на снимке, предоставленном при обновлении, и записи результата обратно в родительский узел. Каждый раз, когда пользователь добавляет или удаляет сеанс медитации, статистика для этого пользователя автоматически пересчитывается. Поскольку статистика просто находится в базе данных, мое приложение может прочитать результат с помощью обычных вызовов API Firebase.

exports.countStreaks = functions.database.ref('diary/{userUid}/entries')
  .onUpdate(async (snapshot, context) => {
  const afterEntries = Object.entries(snapshot.after.val());
  const processedEntries = [];
    
  for (const [key, value] of afterEntries) {    
    const finishTime = DateTime.fromMillis(value.finishTime);
    const diaryEntry = {finishTime, totalSeconds: value.totalSeconds};
    processedEntries.push(diaryEntry);
  }
  const stats = statscalculator.longestStreak(userTimezone, processedEntries);
  if(stats){
    return snapshot.after.ref.parent.child('stats').set(stats);
  }
});

Резюме

Ну это все. Я могу развернуть функцию с конвейером Github Actions CI/CD, как я описал в своей истории создания API с использованием функций Firebase. Я получаю масштабирование до нуля благодаря взиманию платы только за использование GCP, и мой код все еще находится в Node.js, поэтому не нужно изучать новые языки!

Бум.