Архитектура

Архитектура

0.4 NEW — обзор того, как устроен WASM-runtime и его жизненный цикл.

Из чего состоит

lampac-go/
├── internal/wasmmodules/        ← хост: wazero + ABI + permissions + studio
├── wasm_sdk/
│   ├── tinygo/                  ← Go SDK для плагинов (импортируется через replace)
│   └── rust/                    ← Rust SDK (cargo path-зависимость)
├── wasm_modules/                ← живые плагины (manifest.json + plugin.wasm)
│   ├── echo/                    ← server-плагин с полным host API
│   ├── quality_filter/          ← middleware: фильтрует результаты по качеству
│   └── welcome_toast/           ← client: Lampa.Noty.show один раз
├── wasm_dumps/                  ← crash-dumps (ts.json на каждое падение)
├── wasm_studio/                 ← staging для Browser Studio (build artefacts)
└── wwwroot/
    ├── plugins/wasm_loader.js   ← runtime для client-плагинов в Lampa
    └── wasm_studio.html         ← Monaco-редактор для админа

Жизненный цикл серверного плагина

  1. Scan — на старте wasmmodules.Manager.Scan() обходит wasm_modules/, парсит каждый manifest.json, читает plugin.wasm в память.
  2. Lazy-instantiate — на первый запрос /lite/{id} создаётся wazero.Runtime, регистрируются хост-импорты (модуль lampac), вызывается _initialize (если есть), плагин остаётся загруженным.
  3. Invoke — на каждый запрос: маршалим Invocation в JSON, кладём в guest memory через экспорт alloc(size), вызываем handle(ptr, len), читаем ответ.
  4. Hot-reloadfsnotify watcher на wasm_modules/ ловит change/create на manifest.json и *.wasm, через 300 мс debounce делает Scan(). Старая инстанция закрывается, новая создастся при следующем запросе.
  5. Crash — любая ошибка handle() пишет дамп в wasm_dumps/{id}/{unix_ns}.json: invocation, путь, headers, размер памяти, последние 50 строк лога плагина. Хранится 20 последних на модуль.

Manifest

{
  "id": "my_plugin",                           // [a-z][a-z0-9_]{1,31}
  "name": "Display name",
  "version": "0.1.0",
  "target": "server",                          // server|client|both|middleware
  "entry_server": "plugin.wasm",
  "language": "tinygo",                        // info-only, для админки
  "abi_version": 1,                            // host ABI rev — несовместимый ребусит
  "upstreams": ["collaps"],                    // только для middleware
  "permissions": ["http:example.com", "cache"],// см. /docs/wasm/permissions
  "config_schema": [
    {"key": "host", "label": "API host", "type": "string", "default": "https://api.example.com"}
  ]
}

Все поля опциональны кроме id и target. entry_server по умолчанию — plugin.wasm.

Reactor-mode

TinyGo с -target wasi по умолчанию вызывает _start, после которого модуль закрывается. Хост обходит это:

cfg := wazero.NewModuleConfig().WithStartFunctions() // пустой список
inst, _ := wz.InstantiateWithConfig(ctx, wasmBytes, cfg)
if init := inst.ExportedFunction("_initialize"); init != nil {
    init.Call(ctx) // запускаем reactor-режим вручную
}

Это значит плагин должен экспортировать alloc(size i32) i32 и handle(ptr i32, len i32) i64, и желательно не вызывать os.Exit / panic.

Telemetry

Manager.Stats(id) возвращает:

Поле Что считает
invocations / errors Счётчики handle()-вызовов
p50_us / p95_us / p99_us Latency-перцентили (последние 256 вызовов)
http_requests / http_bytes_in/out Что плагин выкачал через host_http
cache_hits / cache_misses / cache_hit_rate Эффективность host_cache_*
last_error / last_error_at / last_ok_at Для столбца “что сломалось” в админке

Что дальше