Bridge (мост JS ↔ Native)
Соответствует пункту 6 задачи: жизненный цикл вызова, передача данных, Promise API, события, стримы, ошибки, сериализация. Только архитектура, без реализации. Модель зафиксирована в ADR-002.
Bridge — самая ответственная часть платформы: канал между JavaScript (внутри WebView) и нативным кодом (Runtime/плагины). Состоит из JS-стороны, native-стороны и транспорта, зависящего от движка WebView.
1. Модель: асинхронный обмен сообщениями
Aurobore использует asynchronous message passing (как Tauri/Capacitor): стороны обмениваются сериализованными запросами и ответами. Это безопаснее прямого доступа к функциям — получатель волен отклонить вредоносный/некорректный запрос. Из этой модели вытекают два примитива:
- Command (invoke) — запрос «вызови метод и верни результат» → Promise.
- Event — однонаправленное «выстрелил-и-забыл» сообщение (lifecycle, изменения состояния), может идти в обе стороны. Стримы строятся поверх событий.
2. Структура сообщений (концептуально)
Все сообщения — JSON-сериализуемы и версионируются полем протокола. Концептуальные виды:
Запрос вызова (JS → native):
{
type: "invoke",
protocol: 1, // версия протокола моста
id: "c-128", // уникальный id вызова (для корреляции)
plugin: "Camera",
method: "getPhoto",
args: { quality: 80 },
meta: { stream: false } // флаг подписки/стрима
}Ответ (native → JS):
{ type: "response", id: "c-128", ok: true, result: { … } }
{ type: "response", id: "c-128", ok: false, error: { code, message, data } }Событие (native → JS или JS → native):
{ type: "event", name: "pause", data: { … } }Сообщение стрима (native → JS):
{ type: "stream", subscriptionId: "s-7", phase: "data"|"error"|"complete", payload: { … } }Конкретный формат фиксируется в спецификации протокола; здесь — модель.
3. Жизненный цикл вызова (полный путь)
JS Transport Native
─────────────────────────────────────────────────────────────────────────────
1. SDK: Aurobore.Camera.getPhoto(args)
2. Bridge(JS): id = next(); создать Promise; pending[id] = {resolve,reject}
3. Bridge(JS): сериализовать запрос ───────────▶
4. доставка ───────────────────▶ 5. Bridge(native): десериализовать
6. валидировать: protocol, plugin, method,
разрешения, область (scope), схему args
7. Plugin Manager → Plugin.method(args)
8. плагин выполняет (возможно async,
вне UI-потока)
9. Bridge(JS): pending[id].resolve(result) ◀──── доставка ◀────── 10. сформировать response{id, ok, result}
11. SDK: await возвращает resultПри ошибке шаги 6–8 формируют response{ ok:false, error }, который на шаге 9 приводит к reject.
Корреляция и таблица ожидания
- Каждому вызову присваивается уникальный
id. - JS-сторона хранит
pending: Map<id, {resolve, reject, timeout?}>. - По ответу с этим
idPromise завершается, запись удаляется. - Опциональный таймаут вызова →
rejectошибкойBRIDGE_TIMEOUT(защита от зависших операций).
4. Promise API
- Базовая модель ответа — Promise (
async/await). Никаких обязательных callback-интерфейсов. - Один
invoke↔ один результат ↔ одно завершение Promise. - Отмена: для отменяемых операций поддерживается
AbortSignal(JS отправляет «cancel» с тем жеid).
5. События
- Доставляются через Event System.
- native → JS: системные (lifecycle) и пользовательские (от плагинов) события.
- JS → native: приложение может эмитить события, на которые подписан натив/плагин.
- Семантика «выстрелил-и-забыл»: доставка без подтверждения, без результата.
6. Стримы (подписки)
Для многократной доставки данных (геолокация, сенсоры, прогресс загрузки):
1. JS: Aurobore.Geolocation.watch(cb)
2. Bridge(JS): invoke c meta.stream=true; subscriptionId = id;
subscriptions[subscriptionId] = cb
3. Native: плагин стартует источник данных, шлёт серию { type:"stream", phase:"data" }
4. JS: на каждое сообщение вызывает cb(payload)
5. Завершение: phase:"complete" или ошибка phase:"error"; отписка из JS останавливает источникДополнительно (SHOULD): батчинг высокочастотных событий и backpressure, чтобы не перегружать JS-поток (FR-B8). Подробности — event-system.md.
7. Сериализация и передача данных
- Базовый формат — JSON (структурированные, JSON-сериализуемые данные). Просто, переносимо между движками, совместимо с моделью Capacitor/Tauri.
- Бинарные данные не гоняем как base64 в JSON (дорого). Варианты (SHOULD, FR-B7):
- blob/ArrayBuffer-каналы, где движок это поддерживает;
- возврат ссылки на ресурс (путь через Asset Loader / безопасную схему), которую веб грузит как URL (например, фото камеры — это URL ресурса, а не байты в ответе).
- Типы данных, проходящие через мост, описаны в манифесте плагина → из них генерируются TS-типы.
- Большие полезные нагрузки — потенциально чанкуются/сжимаются (COULD, FR-B9).
8. Ошибки
Единая структура ошибки моста:
{ code: "CAMERA_PERMISSION_DENIED", message: "…", data?: { … } }- Пространства имён кодов:
BRIDGE_*(транспорт/протокол),<PLUGIN>_*(ошибки плагина),RUNTIME_*(контейнер). - Любая ошибка native-стороны доставляется в JS как reject Promise со структурой выше (NFR-7).
- Категории: ошибка протокола/версии, отказ разрешения, неверные аргументы, таймаут, отмена, внутренняя ошибка плагина, недоступность возможности на данном устройстве/версии.
- В SDK поверх этого — типизированные классы ошибок для удобной обработки.
9. Безопасность
- Проверка источника: мост принимает сообщения только из доверенного контекста приложения (origin/контекст WebView), отбрасывая посторонние.
- Разрешения и области (scopes): перед выполнением метода native-сторона проверяет, что у приложения есть нужное разрешение и вызов попадает в допустимую область (вдохновлено permissions/capabilities Tauri).
- Идентификация транспорта: где это возможно (Chromium/CEF), используется механизм, исключающий вызовы из недоверенного содержимого (аналог
invoke_keyTauri). - Валидация аргументов по схеме из манифеста до передачи в плагин.
- Bridge-скрипт инъектируется Runtime до загрузки контента приложения.
10. Транспорт
Транспорт — низкоуровневая доставка сообщений. Целевой движок — Chromium/CEF (ADR-004). Сохраняется тонкий внутренний шов (один интерфейс), но реальная реализация одна; Gecko не поддерживается.
| Реализация транспорта | Механизм (концептуально) |
|---|---|
| WebView (основная, MVP) | ru.auroraos.WebView: JS sendAsyncMessage(channel, data) ↔ QML onRecvAsyncMessage + addMessageListener; native→JS через runJavaScript. Канал протокола: aurobore:bridge. Реализация: WebViewTransport (packages/bridge-js). |
| CEF (уровень ниже) | CefMessageRouter / window.cefQuery — деталь CEF под wrapper-API; не публичный контракт интеграции. |
| Dev/тест (loopback) | In-memory транспорт для модульного тестирования моста без устройства. |
Верхние уровни моста (корреляция, Promise, события, стримы, ошибки) не зависят от транспорта. Шов оставлен ради тестируемости (loopback) и изоляции возможных изменений CEF/qtium-driver, а не ради второй платформенной реализации.
Примечание: Qt WebChannel не используется — это автоматический мост только для QtWebEngine, а не для CEF-WebView Авроры. Интеграция MVP — WebView async API (aurora/webview.md §5);
CefMessageRouter/cefQuery— деталь CEF ниже wrapper-API. Авторов плагинов транспорт не касается: они работают с контрактом Native SDK.
11. Производительность
- Минимизировать число пересечений границы JS↔native (батчинг, где уместно).
- Тяжёлые/бинарные данные — через ссылки на ресурсы, не через сериализацию.
- Стримы — с backpressure; UI-поток не блокируется.
- DevTools (SHOULD) логируют тайминги вызовов для профилирования (FR-D3).
12. Связи
- ↔ Runtime: владелец native-стороны и транспорта.
- ↔ Plugin Manager: адресат вызовов.
- ↔ Event System: доставка событий/стримов.
- ↔ TypeScript SDK: типизированная обёртка над
invoke/подписками.