Skip to content

Межа даних: чому ui-chats не залежить від chat-web-sdk

Design decision (ADR-стиль). Статус: прийнято.

Контекст

@webitel/ui-chats — презентаційний Vue-пакет, що відмальовує UI чатів (ChatContainer та внутрішні компоненти). Дані надходять з @webitel/chat-web-sdk — тонкої обгортки над згенерованими бекенд-DTO (WebitelImApiGatewayV1*, orval). Ці бекенд-контракти фронтенд-команда не контролює — може впливати лише на саму обгортку.

Питання: чи робити chat-web-sdk залежністю ui-chats і передавати його IMessage/IThread напряму в UI, чи ui-chats оголошує власні вхідні інтерфейси, а конвертацію SDK→UI виконує застосунок-споживач?

Рішення

ui-chats тримає власні вхідні типи (ChatMessageType, ChatMember, ChatMessageFile) як опублікований контракт. Пакет не залежить від chat-web-sdk. Конвертація chat-web-sdk → типи ui-chats — обов'язок застосунку-споживача (agent-workspace-app) як anti-corruption layer.

Обґрунтування

1. Напрям залежностей

chat-web-sdk — волатильний: його типи розширюють raw бекенд-DTO, а бекенд поза контролем фронтенд-команди. ui-chats — стабільний переюзабельний UI. Стабільне не має залежати від нестабільного. Прив'язка UI до SDK → UI ламається щоразу, як бекенд змінює поле.

2. Переюзабельність

ui-chats живе в webitel-ui-sdk (спільний UI-kit). Залежність від chat-web-sdk затягнула б у нього весь SDK + @webitel/api-services (generated models) + транспортний шар. Постраждали б Storybook, тести та інші потенційні споживачі, яким не потрібен саме цей бекенд.

3. Поведінкові сутності не місце в презентації

IMessage/IThread — не plain data, а інстанси класів з методами (markRead, sendMessage) і ServiceConfigurable, що несе transport config. Передати таке в message-bubble = протягнути сервісний/транспортний шар у презентацію. Симптом уже видно: store в застосунку тримає їх у shallowRef, бо це живі об'єкти з методами.

4. Тестованість

Plain ChatMessageType — тривіальні фікстури. IMessage вимагає фабрик/моків із config. Простий вхідний контракт робить компоненти дешевими для тесту.

5. Володіння конвертацією

Мапити один світ в інший може лише той, хто знає обидва — це composition-шар, тобто застосунок. Це класичний anti-corruption layer.

Наслідки

  • Потрібен mapper IMessage → ChatMessageType, що годує the-chat-window.vue, де :messages наразі []. Прецедент трансформації — inline-маппінг у chat-preview.vue. Де живе цей mapper — див. секцію нижче.
  • Ціна: boilerplate маппінгу + два набори типів треба тримати в синку. Це свідома ціна за здоровий шов.
  • ChatMessageType трактується як публічний API ui-chats: чистимо від бекенд-присмаку (channelId, via, raw-string member.type), документуємо, версіонуємо. (Дрібний борг: chat-message.vue читає file.malware, не оголошене в ChatMessageFile — прибрати при формалізації контракту.)

Де живуть адаптери

Sub-decision. Статус: прийнято тимчасово (варіант C) — з явним тригером перегляду на користь B.

Adapter за визначенням залежить від обох пакетів (ui-chats + chat-web-sdk), тож постає окреме питання: у якому пакеті йому жити. Було розглянуто 4 варіанти; жоден не є безкоштовно-чистим — це справжній конфлікт вибору, тому фіксуємо і рішення, і аргументи проти нього.

Розглянуті варіанти

#РозташуванняГоловний аргумент ПРОТИ
C (обрано наразі)subpath export у ui-chats: @webitel/ui-chats/adapters, chat-web-sdk як optional peerDependencyТягне backend-SDK у dependency-граф загального UI-kit; інвертує напрям стабільності (stable ui-chats починає посилатися на unstable chat-web-sdk); реліз UI-kit чіпляється за churn бекенду
Asubpath export у chat-web-sdkchat-web-sdk потенційно публічний (для інтеграторів); не можна тягнути в нього внутрішню UI-либу, якою користується лише продуктова фронтенд-команда
Bокремий glue-пакет @webitel/chat-ui-adapter (обидва як peerDeps)+1 пакет і реліз-цикл на утримання зараз
Dу кожному застосунку окремоДублювання: обидва пакети юзатиме кілька наших фронтенд-апів

Конфлікт вибору

Аргументи тягнуть у різні боки, і це не вирішується «однією правильною відповіддю»:

  • Напрям стабільності (проти C, за A/B): stable не має залежати від unstable; загальний UI-kit не має нести backend-специфічний SDK.
  • Публічність SDK (проти A, за B/C): chat-web-sdk може бути виставлений «в світ» для інтеграторів. Їм adapter під внутрішню либу зайвий, і публічний артефакт не повинен посилатися на приватний UI-пакет.
  • Аудиторія = перетин (за B): adapter потрібен лише тим, хто юзає обидва пакети — це вужче за аудиторію будь-якого з ендпоінтів. Найчистіше — окремий пакет, скоуплений саме під цей перетин.
chat-web-sdk consumers:  багато, зовнішні (інтегратори) — ui-chats НЕ треба
ui-chats consumers:      мало, внутрішні
adapter потрібен:        тільки перетину (юзає обидва) ⊂ внутрішні

Формально «найчистіший» — B. Але наразі свідомо беремо C.

Чому C зараз

  • Один UI-таргет (ui-chats) і жменя внутрішніх споживачів — окремий пакет (B) дає більше накладних (репо/реліз/версіювання), ніж економить.
  • Через subpath export core-вхід @webitel/ui-chats/ui лишається чистим: chat-web-sdk — optional peerDependency, tree-shaking відрізає його для тих, хто імпортує лише /ui. Bundle-cost для не-споживачів adapter'а = 0.
  • Одне джерело правди для ChatMessageType (контракт) і його adapter'а — поряд, у одному репо; синк контракту й маппінгу простіший.

Ціна C і тригер переходу на B

Свідомо приймаємо: реліз ui-chats тепер логічно зчеплений з формами chat-web-sdk (breaking change SDK → апдейт adapter'а → republish).

Мігрувати на B (окремий пакет), коли справдиться будь-що з:

  • з'явиться другий UI-таргет (не лише ui-chats), що мапить з chat-web-sdk;
  • adapter захоче окремий реліз-цикл від ui-chats;
  • churn chat-web-sdk почне помітно смикати релізи UI-kit.

Схема потоку

chat-web-sdk (IMessage[])
      │  @webitel/ui-chats/adapters  (anti-corruption layer; варіант C)

ChatMessageType[]  ──►  <ChatContainer :messages>  (@webitel/ui-chats/ui)

Контракт ui-chats (вхід)

Джерело істини: ui-chats/src/ui/messaging/types/ChatMessage.types.ts. ChatContainer приймає messages: ChatMessageType[]; кожен елемент — plain об'єкт (id, text, createdAt, member, опційний file, тощо). Жодних методів, жодного transport config.