Клиентские плагины

Клиентские плагины

0.4 NEWtarget: client или both: .wasm подгружается прямо в Lampa и расширяет UI/поведение клиента.

Серверные плагины — это балансеры. Клиентские — это полноценные расширения Lampa, написанные на Go/Rust/AssemblyScript: добавляют кнопки, рисуют бейджи на карточках, реагируют на события Lampa, читают/пишут Lampa.Storage.

Зачем не JS

Стандартные Lampa-плагины уже на JS. Зачем нужен WASM-вариант:

  • Один и тот же код на сервере и клиенте, если плагин делает обе части (target: both).
  • Типобезопасность — Go/Rust ловит ошибки на компиляции.
  • Переиспользование — крипто, парсеры, бизнес-логика, написанные для бэкенда, работают в Lampa без портирования.
  • Защита кода.wasm бинарь читать сложнее, чем минифицированный JS.

Что есть в Lampa

Когда target: client или both, lampac-go:

  1. Подключает wwwroot/plugins/wasm_loader.js к каждой Lampa-сессии (через lampainit.js).
  2. Loader на старте Lampa запрашивает /wasm/index.json — каталог всех client-плагинов.
  3. Для каждого делает WebAssembly.instantiateStreaming с per-plugin host-импортами.
  4. Вызывает _initialize (если есть) и затем on_load.
  5. Если у плагина есть экспорт on_event — подписывается на Lampa.Listener.follow('full', ...) и форвардит события в плагин.

Manifest

{
  "id": "welcome_toast",
  "name": "Welcome toast",
  "version": "0.1.0",
  "target": "client",
  "entry_client": "client.wasm",
  "language": "tinygo",
  "abi_version": 1,
  "tags": ["ui", "demo"]
}

target: both требует обоих entry-полей: entry_server (балансер) и entry_client (Lampa-extension). Это два разных .wasm файла, но один манифест и одна метадата для маркетплейса.

Host API (импорты lampac_client)

Функция Что делает
lampac.NotyClient(msg) Lampa.Noty.show(msg) — toast
lampac.StorageGet(key) / StorageSet(key, value) Lampa.Storage.get/set с namespace wasm_plugin_<id>:
lampac.EmitEvent(event, jsonData) Lampa.Listener.send(event, JSON.parse(data))
lampac.ActivityPush(activityJSON) Lampa.Activity.push(JSON.parse(...)) — открыть экран
lampac.ClientInfo("…") / Warn / Error / Debug console.{info,warn,error,debug} с префиксом [<id>]

Все ключи в Lampa.Storage префиксованы wasm_plugin_<id>: — плагины не могут читать/писать друг другу.

Минимальный TinyGo-плагин

wasm_modules/welcome_toast/main.go:

package main

import "lampac.cc/sdk/lampac"

func main() {}

//export on_load
func onLoad() {
    if lampac.StorageGet("seen") == "1" {
        return
    }
    lampac.NotyClient("Привет от WASM-плагина 👋")
    lampac.StorageSet("seen", "1")
}

//export on_event
func onEvent(ptr, length uint32) {
    // Получаем все события Lampa как JSON {"type":"...","data":{...}}
    // — здесь решаем, на что реагировать.
}

Сборка:

tinygo build -o client.wasm -target wasi -no-debug ./
# 24 КБ — клиентские плагины без net/json получаются маленькими

После Install через Browser Studio или просто cp в wasm_modules/welcome_toast/ — Lampa подхватит при следующей перезагрузке страницы.

Реакция на события Lampa

//export on_event
func onEvent(ptr, length uint32) {
    raw := lampac.ReadBytes(ptr, length) // []byte JSON
    var ev struct {
        Type string          `json:"type"`
        Data json.RawMessage `json:"data"`
    }
    json.Unmarshal(raw, &ev)

    switch ev.Type {
    case "full":
        // карточка фильма открыта; можно подмешать рейтинги, бейджи и т.д.
    case "activity":
        // сменилась activity (главная, поиск, плеер…)
    }
}

Lampa имеет много типов событий — full, line, activity, player, и т.д. Подписка идёт на 'full' (универсальный канал), плагин фильтрует по type сам.

Открытие новых экранов

activity := `{
    "url": "/lite/my_balancer",
    "title": "My catalog",
    "component": "category_full",
    "page": 1
}`
lampac.ActivityPush(activity)

ActivityPush принимает любой Lampa Activity descriptor — то же, что в JS:

Lampa.Activity.push({ url: "...", component: "category_full", title: "...", page: 1 });

Catalog endpoint

GET /wasm/index.json — что отдаёт client-loader:

{
  "plugins": [
    {
      "id": "welcome_toast",
      "name": "Welcome toast",
      "version": "0.1.0",
      "wasm_url": "/wasm/welcome_toast/client.wasm",
      "manifest_url": "/wasm/welcome_toast/manifest.json"
    }
  ]
}

Только активные client/both плагины, отключённые в админке — пропускаются.

Подводные камни

  1. Lampa.Listener не везде доступен на старте — loader подписывается на app:ready если возможно, иначе зовёт on_load сразу.
  2. .wasm кэшируется браузером на 5 минут — чтобы перезагрузить, либо bump version в манифесте, либо ?v=... в wasm_url.
  3. WASI-shim в loader’е минимальный — если плагин зовёт сложные сисколы (path_open, fd_read), они вернут ENOSYS.
  4. DOM API нет — взаимодействие с UI идёт только через Lampa.*. Если плагину нужен прямой доступ к DOM, расширь wasm_loader.js своим импортом.

both target

Один манифест, два разных .wasm:

{
  "id": "rating_badge",
  "target": "both",
  "entry_server": "server.wasm",   // подсчёт рейтинга на сервере
  "entry_client": "client.wasm"    // отрисовка бейджа на карточке в Lampa
}

Server-часть зовётся через /lite/rating_badge, client-часть подгружается loader’ом. Они общаются через сетевой запрос (HTTPGet со стороны клиента, балансер на сервере).