Proxy-API

Kind-Apps und Web Components kommunizieren mit dem Wippy-Host über die Proxy-Laufzeit (proxy.js). Ihr Code spricht nie direkt mit dieser Laufzeit — Sie importieren benannte Getter aus @wippy-fe/proxy, einer dünnen synchronen Facade darüber. Derselbe Import funktioniert für beide Oberflächen:

  • Micro-Frontend-Apps (view.page) laufen in einem srcdoc-iframe, in das der Host proxy.js injiziert.
  • Web Components (view.component) laufen als ESM-Module in der Host-Seite; der Host stellt @wippy-fe/proxy über die Import-Map bereit.

Wie die Laufzeit in den jeweiligen Kontext geladen wird, beschreibt Proxy & Isolation.

Initialisierung

@wippy-fe/proxy exportiert synchrone Getter — host, api, on, config, state, ws, logger, sanitize, html, loadCss, loadWebComponent, loadByTagName, hostCss, define, classifyLink, installVueWarnSuppressor, addIcons, tailwindConfig. Importieren Sie, was Sie brauchen, und verwenden Sie es direkt. Es gibt kein getWippyApi, keine instance und keinen GetConfig/SetConfig-Handshake, auf den zu warten wäre.

Das Muster der synchronen Getter teilen sich Micro-Frontend-Apps und Web Components:

import { host, api, config, state, ws, logger } from '@wippy-fe/proxy'

host.navigate('/dashboard')
const agents = await api.get('/api/v1/agents')   // api ist axios; das await gilt dem HTTP-Aufruf, nicht dem Beschaffen von `api`
const token = config.auth.token

Iframe- und Web-Fragment-Apps erhalten Lifecycle-Sichtbarkeit über das Proxy-Topic @visibility. Direkte Web Components nicht: Verwenden Sie useHostVisibility() oder useHostVisibilityRefresh() aus @wippy-fe/webcomponent-vue oder die entsprechenden WippyElement-APIs.

Diese Getter sind synchron — host, api, on, config usw. stehen in dem Moment bereit, in dem Ihr Code läuft. Der Host injiziert die Kind-Konfiguration synchron, bevor die Laufzeit lädt (sowohl für view.page-Apps als auch für view.component-Web-Components), sodass die Laufzeit initialisiert ist, bevor Ihr Skript ausgeführt wird. Sie schreiben nie ein await, um einen Getter zu beschaffen, und es gibt keinen GetConfig/SetConfig-Handshake. Das einzige await, das Sie schreiben, gilt einer tatsächlichen asynchronen Operation (ein HTTP-Aufruf über api, ein state-Lesevorgang usw.).

Holen Sie die import-map.json des Ziel-Web-Host-Releases einmal während der Entwicklung und verwenden Sie jeden Schlüssel ihres imports-Objekts als Rollup-External. Das schließt @wippy-fe/proxy ein; pflegen Sie keine External-Liste mit nur einem Paket oder nur den importierten. Holen Sie sie erneut nur, wenn sich das Web-Host-Tag ändert oder wenn Sie eine Abhängigkeit hinzufügen und prüfen wollen, ob ihr exakter Specifier external sein kann:

// vite.config.ts (nachdem die geholte Antwort als import-map.json gespeichert wurde)
import { readFileSync } from 'node:fs'

const hostImportMap = JSON.parse(
  readFileSync(new URL('./import-map.json', import.meta.url), 'utf8'),
)

export default defineConfig({
  build: {
    rollupOptions: {
      external: Object.keys(hostImportMap.imports),
    },
  },
})

TypeScript-Typen

Die Proxy-Typen — AppConfig, ProxyApiInstance, StateApi, ProxyWsApi und die WebSocket-Nachrichtentypen — werden als Ambient-Deklarationen in @wippy-fe/types-global-proxy ausgeliefert, nicht als benannte Exporte irgendeines Pakets. Fügen Sie es in types Ihrer tsconfig.json ein (oder verwenden Sie eine Triple-Slash-Referenz), und sie sind global verfügbar — ohne Import:

// tsconfig.json
{ "compilerOptions": { "types": ["@wippy-fe/types-global-proxy"] } }
// AppConfig, ProxyApiInstance, … sind Ambient-Globals — direkt damit annotieren, kein Import:
function render(cfg: AppConfig) { /* … */ }
type HostApi = ProxyApiInstance['host']   // HostApi ist dieser indizierte Typ, kein separater Export

Es gibt kein import … from '@wippy-fe/shared' für die obigen Proxy-APIs. @wippy-fe/shared trägt paketübergreifende Typen und GLOBAL_*-Namenskonstanten; ab 0.0.52 exportiert es zusätzlich die Laufzeit-Hilfsfunktionen für gehaltene WCs readWippyVisibility, setWippyVisibility und WIPPY_VISIBILITY_ATTRIBUTE. Autoren direkter WCs verwenden normalerweise useHostVisibility() oder useHostVisibilityRefresh() aus @wippy-fe/webcomponent-vue; das Proxy-Event @visibility bleibt ein Kanal für iframes/Web Fragments.

Interna (nicht verwenden)

Die Laufzeit installiert eine Handvoll Globals für den Eigenbedarf — window.$W, window.getWippyApi, window.initWippyApi und den window.__WIPPY_*-Satz. Anwendungs- und Komponentencode darf sie niemals lesen oder überschreiben. Gehen Sie stattdessen immer über @wippy-fe/proxy. Sie werden nur aufgeführt, damit Sie sie nicht versehentlich überschreiben — siehe Proxy & Isolation § Interna.

@wippy-fe/proxy (hier dokumentiert) ist die API, die Ihr Kind-Code verwendet. Der Bootstrap des Hosts selbst, initWippyApp(config, rootContainer?), mountet den gesamten Web Host auf dem Modul-Embed-/Facade-Pfad — Kind-App-Code ruft ihn nie auf.


Konfiguration

config

Die vom Host gelieferte Konfiguration der Kind-Anwendung. Es ist ein einfaches Objekt (keine Funktion) — direkt importiert und synchron lesbar. Neue Dokumente zielen nur auf den aktuellen Vertrag wippy-context-2.0.

import { config } from '@wippy-fe/proxy'

const token = config.auth.token
interface ChildAppConfig {
  $schema: 'wippy-context-2.0'
  auth: {
    token: string
    expiresAt: string
  }
  env: {
    APP_API_URL: string
    APP_AUTH_API_URL: string
    APP_WEBSOCKET_URL: string
    [key: string]: string | undefined
  }
  axiosDefaults?: Partial<AxiosDefaults>
  routePrefix?: string
  apiRoutes?: Record<string, string>
  themeMode?: 'auto' | 'light' | 'dark'
  theming: {
    global?: {
      customCSS?: string
      cssVariables?: Record<string, string>
      icons?: Record<string, unknown>
      iconSets?: Record<string, Record<string, unknown>>
    }
  }
  context: {
    resourceId: string
    resourceType: 'page' | 'artifact'
    route?: string
    [key: string]: unknown
  }
  selfPageId?: string
  mountRoutes?: Record<string, string>
}

Für dynamische Seiten gilt, wenn die Host-URL /c/page-id/something/else?foo=1 lautet:

  • config.context?.route trägt /something/else?foo=1.
  • config.path ist ein veraltetes Kompatibilitätsfeld aus Payloads vor wippy-context-2.0 und sollte in neuem Code nicht verwendet werden.

Host-Steuerung

host

Die API zur Host-Kommunikation (HostApi). Direkt importiert und synchron verwendet.

import { host } from '@wippy-fe/proxy'

host.setThemeMode(mode) und host.getThemeMode()

Der Theme-Modus ist Host-Zustand, der von der AppConfig transportiert wird. Wechseln Sie ihn nur über die öffentliche Proxy-API:

import { host, on } from '@wippy-fe/proxy'

async function setThemeMode(mode: 'auto' | 'light' | 'dark') {
  await new Promise<void>((resolve, reject) => {
    const unsubscribe = on('@theme', (appliedMode) => {
      if (appliedMode !== mode) return
      unsubscribe()
      const currentMode = host.getThemeMode()
      if (currentMode !== mode) {
        reject(new Error(`Theme propagation mismatch: ${currentMode}`))
        return
      }
      resolve()
    })

    // Vor dem Kommando abonnieren, damit ein schnelles Propagationsereignis nicht verloren geht.
    host.setThemeMode(mode)
  })
}

await setThemeMode('dark')

Die akzeptierten Modi sind auto, light und dark. auto folgt der Betriebssystem-Voreinstellung. Eine Änderung wird auf den Host angewandt, in die AppConfig zurückgeschrieben, an aktive Seiten-iframes und Web Components gesendet und durch verschachtelte Wippy-Container weitergereicht. Abonnieren Sie @theme, wenn Code auf den angewandten Kind-Zustand warten muss. Geben Sie das Abonnement beim Unmount der Komponente frei.

Der Host besitzt die Persistenz nicht. Die einbettende Facade lauscht auf das Theme-Änderungsereignis des Hosts und persistiert die Benutzerwahl wie in Theme-Persistenz beschrieben.

Fügen Sie keine w-theme-dark- / w-theme-light-Klassen hinzu oder entfernen sie, rufen Sie nicht das interne applyThemeMode auf, mutieren Sie keine AppConfig-Stores, erzeugen Sie keine Proxy-Nachrichten und verwenden Sie kein window.getWippyApi. Das sind Implementierungsdetails des Web Host, keine Anwendungs- oder Browser-Test-APIs. Laufzeittests müssen host.setThemeMode() verwenden, auf das propagierte @theme-Event warten und host.getThemeMode() prüfen, bevor sie das Erscheinungsbild erfassen. Die AppConfig ist der Transport vom Host zum Kind; mutieren Sie ihren internen Store nicht und verlassen Sie sich nicht auf einen früher importierten Konfigurations-Snapshot als Abschlusssignal.

Es gibt keine Methode host.applyTheme().


host.startChat(agentToken, options?)

Öffnet eine neue Chat-Sitzung mit dem angegebenen Agent-Start-Token.

host.startChat(agentToken: string, options?: { sidebar?: boolean }): void
Parameter Typ Standard Beschreibung
agentToken string — Token, das angibt, welcher Agent gestartet werden soll
options.sidebar boolean false true öffnet den Chat im rechten Sidebar-Panel; false öffnet ihn im Hauptbereich
host.startChat('my-agent-token')                     // Hauptbereich
host.startChat('my-agent-token', { sidebar: true })  // Rechte Sidebar

host.openSession(sessionId, options?)

Öffnet eine bestehende Chat-Sitzung anhand ihrer UUID.

host.openSession(sessionId: string, options?: { sidebar?: boolean }): void
host.openSession('abc-123-uuid', { sidebar: false })

host.navigate(url)

Fordert SPA-Navigation vom Host an. Unterstützte Muster:

  • /c/<page-id> — zu einer dynamischen Seite navigieren
  • /c/<page-id>/<sub-path> — dynamische Seite mit Unterpfad
  • /chat/<session-id> — eine Chat-Sitzung öffnen
  • Jede Mount-Route, die eine Seite mit mountRoute in ihrem Registry-Eintrag beansprucht
host.navigate(url: string): void
host.navigate('/c/my-page-id')
host.navigate('/chat/session-uuid')
host.navigate('/keeper')

Einschränkung bei verwaltetem Layout. startChat, openSession, openArtifact und navigate zielen auf die Standard-Kompatibilitätshülle (die Chat-Ansicht, das rechte Panel und die Root-Route). Bei fe_mode = managed werden sie zwar weiterhin ausgelöst, haben aber keine eingebaute Renderfläche — rendern Sie Chat, Artefakte und Unterrouten stattdessen über deklarierte Panels. Siehe Multi-Panel-Layout § Was in welchem Modus funktioniert.


host.onRouteChanged(internalRoute, navId?) — Low-Level-Router-Integration

Benachrichtigt den Host, wenn sich die interne Route der Seite ändert. Der Host aktualisiert die URL-Leiste des Browsers, sodass sie die Route des Kindes enthält. Dieser Aufruf ist erforderlich — ohne ihn bleibt die Host-URL auf dem Seiten-Root, und der Zurück-Button des Browsers funktioniert für die Kind-Navigation nicht.

host.onRouteChanged(internalRoute: string, navId?: number): void

Portable Vue-Anwendungen verwenden createAppRouter() aus @wippy-fe/router; das Paket besitzt diesen Aufruf, das zugehörige @history-Abonnement, die Normalisierung und die Unterdrückung von Echo-Schleifen. Verdrahten Sie diese Teile nicht manuell im Anwendungscode. Diese Methode bleibt für Autoren von Plattform-Adaptern und für Nicht-Vue-Integrationen dokumentiert.


host.confirm(options) → Promise<boolean>

Zeigt einen PrimeVue-Bestätigungsdialog. Löst mit true auf, wenn der Benutzer zustimmt, mit false, wenn er ablehnt oder abbricht.

host.confirm(options: LimitedConfirmationOptions): Promise<boolean>
const confirmed = await host.confirm({
  message: 'Delete this item permanently?',
  header: 'Confirm Delete',
  icon: 'tabler:trash',
  acceptLabel: 'Delete',
  rejectLabel: 'Cancel',
  acceptClass: 'p-button-danger',
})

if (confirmed) {
  await api.delete('/api/v1/items/123')
}

host.toast(options)

Zeigt eine PrimeVue-Toast-Benachrichtigung.

host.toast(options: ToastMessageOptions): void
severity Erscheinungsbild
success Grün
info Blau
warn Gelb
error Rot
host.toast({
  severity: 'success',
  summary: 'Saved',
  detail: 'Your changes have been saved.',
  life: 3000,
})

host.openArtifact(artifactUUID, options?)

Öffnet ein Artefakt in der Sidebar oder in einem Modal.

host.openArtifact(
  artifactUUID: string,
  options?: { target?: 'sidebar' | 'modal' }
): void

Das Standardziel ist 'sidebar'.

host.openArtifact('artifact-uuid-123', { target: 'modal' })

host.setContext(context, sessionUUID?, source?)

Sendet Kontextdaten an die aktuelle Chat-Sitzung. Ist noch keine Sitzung offen, wird der Kontext in die Warteschlange gestellt und auf die nächste über startChat oder openSession geöffnete Sitzung angewandt. Optional lässt sich der Kontext auf eine bestimmte Sitzungs-UUID beschränken oder mit einem Quellen-Deskriptor kennzeichnen.

host.setContext(
  context: Record<string, unknown>,
  sessionUUID?: string,
  source?: { type: 'page' | 'artifact', uuid: string, instanceUUID?: string }
): void
host.setContext({
  currentPage: 'dashboard',
  selectedItemIds: [1, 2, 3],
})

host.classifyLink(url) → LinkClassification

Klassifiziert ein href als host-nav, child-nav, external oder ignore. Nutzt mountRoutes und routePrefix aus der Kind-Konfiguration plus eingebackene Systemrouten-Segmente. Reine Funktion — keine Seiteneffekte.

host.classifyLink(href: string): LinkClassification

interface LinkClassification {
  kind: 'host-nav' | 'child-nav' | 'external' | 'ignore'
  href: string
  normalizedPath?: string
  targetPageId?: string  // gesetzt, wenn host-nav eine bestimmte mountRoute getroffen hat
}
// Anchor-Handler, der den Klassifizierer nutzt
import { host } from '@wippy-fe/proxy'

document.addEventListener('click', (ev) => {
  const a = (ev.target as HTMLElement)?.closest('a')
  if (!a) return
  const cls = host.classifyLink(a.getAttribute('href') ?? '')

  if (cls.kind === 'host-nav') {
    ev.preventDefault()
    host.navigate(cls.normalizedPath ?? cls.href)
  }
  // child-nav / external / ignore: bestehende Handler laufen lassen
})

Ersetzen Sie in Vue-Apps RouterLink aus vue-router durch RouterLink aus @wippy-fe/router — es nutzt intern classifyLink und ist prop-kompatibel mit dem echten RouterLink.


host.handleError(code, error)

Meldet einen Fehler zur zentralen Behandlung an den Host.

host.handleError(
  code: 'auth-expired' | 'other',
  error: Record<string, unknown>
): void
  • 'auth-expired' — löst den Re-Authentifizierungsfluss des Hosts aus
  • 'other' — allgemeiner Fehler; wird geloggt und dem Benutzer angezeigt, sofern angemessen
try {
  await api.get('/protected-endpoint')
} catch (error) {
  if ((error as any).response?.status === 401) {
    host.handleError('auth-expired', error as Record<string, unknown>)
  } else {
    host.handleError('other', error as Record<string, unknown>)
  }
}

host.logout()

Meldet den aktuellen Benutzer ab und beendet seine Sitzung.

host.logout(): void

host.bridge

Kanalbasiertes Parent-Kind-Messaging, wenn die Seite in einem <w-iframe> eingebettet ist. Das vollständige Protokoll beschreibt Proxy & Isolation § Parent-Kind-Brücke.

// Fire-and-forget an den Parent
host.bridge.post(channel: string, payload?: unknown): void

// Request/Response (löst mit dem Rückgabewert des Parent-Handlers auf)
host.bridge.request<T>(
  channel: string,
  payload?: unknown,
  options?: { timeoutMs?: number }
): Promise<T>

// Handler für eingehende Nachrichten vom Parent registrieren
host.bridge.on(
  channel: string,
  handler: (payload: unknown) => unknown | Promise<unknown>
): () => void  // liefert die Abmeldefunktion

Lassen Sie options.timeoutMs weg, verwendet host.bridge.request() standardmäßig eine Frist von 10 Sekunden (10000 ms). Bei Zeitüberschreitung wird das zurückgegebene Promise mit einem Error abgelehnt, dessen Nachricht Bridge request <id> timed out after <ms>ms lautet. Eine Anfrage an einen Kanal, für den der Parent keinen Handler hat, wird sofort mit No handler registered for channel "<channel>" abgelehnt, statt die Frist auszusitzen.


host.layout

Zugriff auf die API des verwalteten Layouts. Nur verfügbar, wenn hostConfig.layout gesetzt ist (also fe_mode = managed). Außerhalb dieses Kontexts ist host.layout.snapshot gleich null, und mutierende Aufrufe sind No-Ops.

const layout = host.layout

// Aktuellen Snapshot lesen
if (layout.snapshot) {
  console.log(layout.snapshot.activeBreakpoint)  // 'default' | 'sm' | ...
  console.log(layout.snapshot.panels)             // Map der Panel-Definitionen
  console.log(layout.snapshot.layouts)            // Panel-Bäume nach Breakpoint
}

// Änderungen abonnieren (der frische Snapshot wird an den Handler übergeben)
import { on } from '@wippy-fe/proxy'

on('@layout-change', (snapshot) => {
  console.log(snapshot.activeBreakpoint)
})

// Mutationen
layout.resizePanel('right', '40%')
layout.collapsePanel('nav')
layout.expandPanel('nav')
layout.movePanel('right', { relativeTo: 'main', position: 'after' })
layout.removePanel('right')
layout.updatePanel('right', { kind: 'page', id: 'chat-panel' })  // {kind,id} ersetzt den Inhalt vollständig
layout.updatePanel('right', { props: { artifactId: 'abc-123' } })  // {props} wird flach in die bestehenden Props gemergt

layout.addFloating('flap', {
  kind: 'component',
  tagName: 'w-right-flap',
  position: { x: 0, y: 200 },
  size: { width: 48, height: 80 },
  dismissable: false,
})
layout.removeFloating('flap')
layout.closeModal('confirm-discard')

// Bus innerhalb des Tabs
layout.broadcast('open-chat', { token: 'abc' })       // 1:N (Sender ausgenommen)
layout.send('right', 'open-chat', { token: 'abc' })   // 1:1 an ein benanntes Panel

const off = layout.on('open-chat', ({ payload, sourcePanelId, targetPanelId }) => {
  // behandeln
})
off()  // abmelden

Das vollständige Modell des verwalteten Layouts beschreibt Multi-Panel-Layout.


API

api

Eine vorkonfigurierte axios-Instanz mit:

  • Basis-URL aus der Deployment-Umgebung
  • automatischer Injektion von Authorization: Bearer <token> bei jeder Anfrage
import { api } from '@wippy-fe/proxy'

const response = await api.get('/api/v1/users')
const result   = await api.post('/api/v1/items', { name: 'New item' })

Datei-Upload

import { api, on } from '@wippy-fe/proxy'

const formData = new FormData()
formData.append('file', file)

const abort = new AbortController()

const response = await api.post('/api/v1/uploads', formData, {
  signal: abort.signal,
  headers: { 'Content-Type': 'multipart/form-data' },
  onUploadProgress: (evt) => {
    if (!evt.total) return
    const pct = Math.round((evt.loaded * 100) / evt.total)
    uploadProgress.value = pct
  },
})

const uploadedUuid = response.data.uuid  // { success: boolean, uuid: string }

// Verarbeitungsstatus über WebSocket verfolgen
on(`upload:${uploadedUuid}`, (msg) => {
  // msg.data.status: 'uploaded' | 'completed' | 'error' | 'processing'
})

// Laufenden Upload abbrechen
abort.abort()

Maximale Dateigröße: 100 MB.

Datei-Download

const response = await api.get('/api/v1/uploads/{uuid}/download', {
  responseType: 'blob',
})

const url = URL.createObjectURL(response.data)
const a = document.createElement('a')
a.href = url
a.download = 'filename.pdf'
a.click()
URL.revokeObjectURL(url)

Upload-Informationen abrufen

// Paginierte Liste
const list = await api.get('/api/v1/uploads/list', {
  params: { limit: 10, offset: 0 },
})
// list.data.uploads: Array<{ uuid, mime_type, size, status, meta: { filename } }>

// Einzelner Upload
const upload = await api.get(`/api/v1/uploads/${uuid}`)
// upload.data: { uuid, mime_type, size, status, meta: { filename, content_sample? } }

SSE-Streaming

Das Proxy-api unterstützt Server-Sent-Event-Streams über den Fetch-Adapter. Verwenden Sie das für token-weise LLM-Vervollständigungen, langlaufende Fortschritts-Streams oder jede text/event-stream-Antwort.

Verwenden Sie nicht das native EventSource des Browsers — es kann keine eigenen Header setzen und daher das Authorization: Bearer-Token des Proxys nicht mitführen.

import { api } from '@wippy-fe/proxy'

const abort = new AbortController()

const response = await api.post('/api/v1/agents/stream', { prompt: 'Hello' }, {
  adapter: 'fetch',          // erforderlich — der Standard-xhr-Adapter puffert den kompletten Body
  responseType: 'stream',
  headers: { Accept: 'text/event-stream' },
  signal: abort.signal,
})

const reader = (response.data as ReadableStream<Uint8Array>).getReader()
const decoder = new TextDecoder()
let buffer = ''

try {
  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })

    while (true) {
      const sep = buffer.indexOf('\n\n')
      if (sep === -1) break
      const rawEvent = buffer.slice(0, sep)
      buffer = buffer.slice(sep + 2)

      const dataLines = rawEvent
        .split('\n')
        .filter(line => line.startsWith('data:'))
        .map(line => line.slice(5).trimStart())

      if (dataLines.length === 0) continue
      const payload = dataLines.join('\n')
      if (payload === '[DONE]') return

      try {
        const evt = JSON.parse(payload)
        handleEvent(evt)
      } catch {
        handleText(payload)
      }
    }
  }
} finally {
  reader.releaseLock()
}

// Den Stream abbrechen
abort.abort()

Um alle Anfragen standardmäßig auf den Fetch-Adapter zu setzen:

// In package.json → wippy.configOverrides oder window.__WIPPY_CONFIG_OVERRIDES__
{
  "axiosDefaults": { "adapter": "fetch" }
}

Surface

Die Geometrie des Bereichs, den der Web Host dieser App zugewiesen hat. Dieser Bereich ist üblicherweise nicht das Browserfenster — die App kann eines von mehreren Panels sein —, sodass window.innerWidth und Viewport-Einheiten die falschen Bezugsgrößen sind. Den vollständigen Vertrag beschreibt Surface-Portabilität, Umstellungsrezepte finden Sie unter Surface-Migration.

host.surface.snapshot

Aktuelle Geometrie, zurückgelesen aus denselben berechneten Custom Properties, die das CSS der App auflöst — sie kann also nicht von dem abweichen, was @container wippy-surface (…) und cqw sehen.

const { contract, revision, engine, sizing, width, widthUnit, height, heightUnit } = host.surface.snapshot
Feld Typ Hinweise
contract 1 Vertragsversion
revision number monoton; steigt, wenn sich die Geometrie ändert
engine 'iframe' | 'fragment' | 'host' host bedeutet, dass keine Surface zugewiesen wurde
sizing 'container' | 'content'
width / widthUnit number volle Breite und 1 % davon, in CSS-Pixeln
height / heightUnit number | null null bei Content-Sizing — die Block-Achse ist tatsächlich nicht verfügbar

host.surface.onChange(listener) → () => void

Abonniert Änderungen der Geometrie. Liefert eine idempotente Abmeldefunktion, die beim Abbau aufgerufen werden muss.

const off = host.surface.onChange((snapshot) => {
  canvas.width = snapshot.width
})

host.surface.supports(capability) → boolean

if (host.surface.supports('block-size')) {
  // die Block-Achse ist verfügbar (Container-Sizing)
}

Capabilities: block-size und surface-scroll werden heute wahrheitsgemäß beantwortet. registered-hit-testing, native-document-hit-testing und owner-visibility sind reserviertes Vokabular und melden immer false.

Bevorzugen Sie supports() gegenüber einer Verzweigung über engine — entscheidend ist, ob eine Capability verfügbar ist, nicht welche Engine rendert.

host.surface.engine und host.surface.sizing

Nur lesende Abkürzungen für dieselben Werte im Snapshot. engine: 'host' bedeutet, dass der Code direkt in das Host-Dokument gemountet ist (oder unter dem eigenständigen Dev-Proxy läuft), ohne zugewiesene Surface; der Snapshot meldet bewusst width: 0 und sizing: 'content'.

engine ist kein verlässlicher Test für "wurde eine Surface zugewiesen". Eine über <w-iframe>/<w-artifact> eingebettete Seite erhält ebenfalls keine Surface — verschachtelte Einbettungen nehmen sich heraus, bis Unterstützung für verschachtelte Surfaces ausgeliefert wird — meldet aber engine: 'iframe' mit width: 0. Prüfen Sie snapshot.width, wenn dieser Unterschied zählt.


Events

on(topic, handler) → () => void

on abonniert Events aus der WebSocket-Schicht des Hosts oder interne Proxy-Events. Liefert eine Abmeldefunktion zurück.

on(topic: string, handler: (event: unknown) => void): () => void

Topics verwenden durch Doppelpunkte getrennte Segmente. * ist ein Wildcard für ein einzelnes Segment. Das Muster muss dieselbe Anzahl Segmente haben wie das Topic, auf das es passt.

import { on } from '@wippy-fe/proxy'

// Abmelden, wenn fertig
const unsub = on('session:abc:message:*', (msg) => {
  console.log(msg.data)
})
unsub()

Jeder on()-Aufruf liefert eine Abmeldefunktion. Rufen Sie sie immer auf, wenn die Komponente unmountet, um Lecks zu vermeiden. Beim Entladen des iframes werden verbleibende Abonnements automatisch bereinigt, aber explizites Aufräumen bleibt für Komponenten erforderlich, die innerhalb eines langlebigen iframes mounten und unmounten.

// Vue Composition API
import { onUnmounted } from 'vue'

const unsub1 = on('session:*:message:*', handler)
const unsub2 = on('artifact:*', handler)

onUnmounted(() => {
  unsub1()
  unsub2()
})
// Vanilla / Web Component
import { on } from '@wippy-fe/proxy'

class MyEl extends HTMLElement {
  private unsubs: Array<() => void> = []

  connectedCallback() {
    this.unsubs.push(on('session:*:message:*', handler))
  }

  disconnectedCallback() {
    this.unsubs.forEach(fn => fn())
    this.unsubs = []
  }
}

Eingebaute Topics

Topic Handler-Payload Beschreibung
@history { path: string } Host-URL geändert (SPA-Navigation). Wird ausgelöst, wenn der Parent eine neue Route pusht.
@visibility boolean Sichtbarkeit von iframe/Web Fragment geändert. Direkte Web Components verwenden stattdessen den typisierten Host-Sichtbarkeitsvertrag.
@message Vollständige WS-Nachricht Alle WebSocket-Nachrichten. Abonniert intern *, *:*, *:*:*, *:*:*:*.
@state-error { error: string, key?: string } Speichervorgang im State ist fehlgeschlagen (Kontingent überschritten, Serialisierungsfehler).
@layout-change LayoutSnapshot Snapshot des verwalteten Layouts aktualisiert; der frische Snapshot wird an den Handler übergeben. Entspricht dem Lesen von host.layout.snapshot.
@layout-breakpoint { name: string, width: number } Aktiver Breakpoint des verwalteten Layouts geändert; name ist der neue Breakpoint, width sein Schwellenwert (px).

Wildcard-Muster

// Nur iframe-/Web-Fragment-Seiten; direkte WCs verwenden useHostVisibility().
on('@visibility', (visible: boolean) => { /* sichtbar oder verborgen */ })

// Alle Sitzungsnachrichten in einer bestimmten Sitzung
on('session:abc-123:message:*', (msg) => { /* ... */ })

// Alle Nachrichten über alle Sitzungen hinweg
on('@message', (msg) => { /* ... */ })

// Topics, deren Teile ':' enthalten, müssen kodiert werden
on(`session:${encodeURIComponent('id:with:colons')}:message:*`, handler)

@history ist der Protokollvollständigkeit halber aufgeführt. Portable Vue-Anwendungen müssen @wippy-fe/router darauf abonnieren lassen; fügen Sie keinen zweiten, von der Anwendung besessenen Handler hinzu.

Dasselbe Topic mehrfach aus demselben Frame zu abonnieren ist sicher. Der Proxy dedupliziert auf Host-Ebene. Jeder on()-Aufruf erhält dennoch seine eigene unabhängige Abmelde-Handle.


State

state — iframe-übergreifende Key-Value-Persistenz

state bietet host-vermittelten Speicher, der die Zerstörung eines iframes überdauert. Der State ist pro Seiten- oder Artefakt-UUID gescopt; jede App erhält einen isolierten Namespace.

Alle Methoden akzeptieren eine optionale Option { scope?: string }, um den Standard-Scope zu überschreiben. Verwenden Sie scope, wenn mehrere Instanzen derselben Komponente getrennte State-Behälter benötigen.

Eindeutigkeit des Scopes: Scope-Werte werden von der rohen state-API unverändert durchgereicht und müssen in Ihrer gesamten Anwendung global eindeutig sein. Das Plugin @wippy-fe/pinia-persist stellt eigenen Scopes automatisch @custom: voran, um Kollisionen mit System-Scopes zu vermeiden.

import { state } from '@wippy-fe/proxy'

// Schreiben (Fire-and-forget; @state-error wird bei überschrittenem Kontingent ausgelöst)
await state.set('filters', { search: 'john', status: 'active' })

// Lesen (liefert null, wenn der Schlüssel nicht gefunden wird)
const filters = await state.get<{ search: string, status: string }>('filters')

// Einen Schlüssel löschen
await state.remove('filters')

// Den gesamten State dieser Seite leeren
await state.clear()

// Alles auf einmal lesen (nützlich für Massen-Hydration)
const all = await state.getAll()

// Eigener Scope
await state.set('count', 42, { scope: 'my-widget-instance-1' })
const count = await state.get<number>('count', { scope: 'my-widget-instance-1' })

Methodensignaturen:

state.get<T = unknown>(key: string, options?: { scope?: string }): Promise<T | null>
state.set(key: string, value: unknown, options?: { scope?: string }): Promise<void>
state.remove(key: string, options?: { scope?: string }): Promise<void>
state.clear(options?: { scope?: string }): Promise<void>
state.getAll(options?: { scope?: string }): Promise<Record<string, unknown>>

Empfohlenes Speichermuster für iframe/Web Fragment — speichern Sie, wenn die Seite in den Hintergrund geht, statt bei jeder Änderung. Direkte WCs verwenden useHostVisibility() für dieselbe Lifecycle-Entscheidung:

on('@visibility', async (visible) => {
  if (!visible) {
    await state.set('scrollY', document.documentElement.scrollTop)
    await state.set('formData', currentFormData)
  }
})

Grenzen: 2 MB pro Seite (JSON-serialisiert, vom Host über hostConfig.stateCache konfigurierbar). Der State liegt im Speicher des Hosts — er überdauert ein iframe-Reload, aber kein vollständiges Neuladen der Browserseite.

Pinia-Integration

Für Vue-Apps mit Pinia automatisiert @wippy-fe/pinia-persist die Persistenz:

import { createWippyPersist, preloadWippyState } from '@wippy-fe/pinia-persist'

const preloaded = await preloadWippyState()
const pinia = createPinia()
pinia.use(createWippyPersist(preloaded))
app.use(pinia)

Markieren Sie dann die Stores:

const useMyStore = defineStore('my-store', () => {
  const filters = ref({ search: '' })
  return { filters }
}, {
  wippyPersist: true,
  // oder: wippyPersist: { pick: ['filters'], debounce: 500 }
})

WebSocket

ws

ws sendet Kommandos über die WebSocket-Verbindung des Hosts. Antworten treffen über on()-Topic-Abonnements ein.

ws.send(command)

Fire-and-forget. Keine Zustellung von Antworten — abonnieren Sie zuerst das relevante Topic.

ws.send(command: WsCommand): void
import { ws, on } from '@wippy-fe/proxy'

on('session:my-session:message:*', (msg) => {
  console.log('Response:', msg.data)
})

ws.send({
  type: 'session_message',
  session_id: 'my-session',
  message_id: crypto.randomUUID(),
  data: { text: 'Hello from child app' },
})

ws.sendWithResponse(command) → Promise<WsMessage>

Sendet ein Kommando und wartet auf die passende Serverantwort. Zeitüberschreitung nach 30 Sekunden.

ws.sendWithResponse(command: WsCommand): Promise<WsMessage>
const response = await ws.sendWithResponse({
  type: 'session_open',
  start_token: 'my-token',
})
console.log('Session opened:', response.data)

ws.sendCommand(sessionId, data)

Bequemer Wrapper für Kommandos zur Sitzungssteuerung.

ws.sendCommand(sessionId: string, data: { command: string, [key: string]: unknown }): void
ws.sendCommand('session-uuid', { command: 'stop' })
ws.sendCommand('session-uuid', { command: 'model', name: 'gpt-4' })
ws.sendCommand('session-uuid', { command: 'agent', name: 'my-agent' })

Logger

logger

Strukturiertes Logging, das iframe-Grenzen überschreitet. Logs fließen vom Kind über den Host zur Parent-Website, wo Transports (Sentry, Graylog, Konsole) sie verarbeiten. Der Kontext jedes Kindes (resourceId, resourceType, Verschachtelungstiefe) wird automatisch an jeden Log-Eintrag angehängt.

Verwenden Sie logger statt console.log/error für alles, was im Produktions-Monitoring erscheinen soll.

import { logger } from '@wippy-fe/proxy'

logger.debug('Component mounted', { pageId: 'abc' })
logger.info('User loaded page', { pageId: 'abc' })
logger.warn('Slow API response', { ms: 3200 })
logger.error('Failed to save', { endpoint: '/api/save' })

logger.captureException(error, context?)

Erfasst eine Ausnahme und leitet sie weiter. Unbehandelte Fehler (window.onerror, unhandledrejection) werden automatisch erfasst, wenn ProxyConfig.injections.errorCapture gleich true ist.

try {
  await riskyOperation()
} catch (error) {
  logger.captureException(error, { operation: 'riskyOperation' })
}
// Breadcrumbs hängen sich als Debugging-Kontext an die nächste Ausnahme
logger.addBreadcrumb({ category: 'navigation', message: 'Navigated to /settings' })
logger.addBreadcrumb({ category: 'ui', message: 'Clicked Save button' })

// Persistenter Kontext — wird an alle nachfolgenden Logs dieses Kindes angehängt
logger.setContext('user', { id: 'user-123', role: 'admin' })

// Tags — Schlüssel/Wert-Paare zum Filtern und Suchen
logger.setTag('version', '1.2.0')
logger.setTag('feature', 'dashboard')

Web Components

loadByTagName(tagName, options?) → Promise<void>

Lädt und registriert eine benachbarte Web Component anhand ihres HTML-Tag-Namens. Löst auf, nachdem customElements.define ausgelöst wurde — es ist sicher, unmittelbar danach document.createElement(tagName) aufzurufen. Bei Erfolg wird das Tag automatisch zur sanitize-Allowlist hinzugefügt.

import { loadByTagName } from '@wippy-fe/proxy'

await loadByTagName('wc-thread-picker')
await loadByTagName('wc-slow-pkg', { timeoutMs: 60_000 })

// Kann sofort verwendet werden
document.body.appendChild(document.createElement('wc-thread-picker'))

options.timeoutMs überschreibt die Standardfrist von 30 Sekunden für das Warten auf customElements.define, nachdem das Skript angehängt wurde. Macht hängende oder defekte Komponenten (404, Parse-Fehler, fehlender define-Aufruf) als Ablehnung sichtbar, statt unbegrenzt zu hängen.

loadWebComponent(componentId, tagName?) → Promise<void>

Lädt eine Web Component über ihre Artefakt-ID in der Wippy-Registry statt über ihren Tag-Namen. Nützlich, wenn Sie eine Registry-ID aus einem Konfigurationswert oder einer Backend-Antwort haben.

import { loadWebComponent } from '@wippy-fe/proxy'

await loadWebComponent('wippy.components:my-chart')

DOM-Scan-Loader (<script type="wippy-components-loader">)

Für Seiten, die mehrere Komponenten benötigen, scannt der Proxy bei der Initialisierung nach diesen Script-Tags und lädt jeden Eintrag über loadWebComponent:

<script type="wippy-components-loader">
{ "wc-foo": "wippy.components:foo", "wc-bar": "wippy.components:bar" }
</script>

Dasselbe Verhalten bei Deduplizierung und automatischer Aktualisierung der Allowlist wie bei loadByTagName.


Hilfsfunktionen

sanitize(html, options?) → string

HTML-Sanitizer mit Standard-Allowlist, gescopt auf den aktuellen Proxy-Kontext. Kombiniert die Standardwerte des Chat-Renderings (<p>, <a>, <code>, <table> usw.) mit jedem Web-Component-Tag, das derzeit in dieser Laufzeit registriert ist.

import { sanitize, loadByTagName } from '@wippy-fe/proxy'

const safe = sanitize('<p>hi</p><script>alert(1)</script>')
// → '<p>hi</p>'

// Nach loadByTagName ist das Tag automatisch erlaubt:
await loadByTagName('wc-thread-picker')
sanitize('<wc-thread-picker thread-id="42"></wc-thread-picker>')
// → '<wc-thread-picker thread-id="42"></wc-thread-picker>'

// Einmalige Zusatz-Tags
sanitize(dialogBody, { extraTags: { 'iconify-icon': ['icon'] } })

sanitize liest die Tag-Allowlist bei jedem Aufruf neu ein, sodass auch nach dem Import registrierte Tags berücksichtigt werden.

html.inject(sourceHtml, options) → Promise<string>

Wendet die Transformation von Quell-HTML zu srcdoc an, ohne ein Element zu mounten. Bevorzugen Sie im Normalfall <w-iframe>; verwenden Sie dies nur, wenn Sie eigene Hosting-Infrastruktur bauen.

import { html } from '@wippy-fe/proxy'

const processed = await html.inject(sourceHtml, {
  baseUrl: 'https://example.com/app/',
  resourceId: 'child-id',
  resourceType: 'page',
  route: '/initial',
})

Konfigurations-Overrides

Seiten können ausgewählte kindgerichtete Konfigurationsfelder pro Seite überschreiben, ohne ein separates Deployment. Die Override-Form verwendet aus Kompatibilitätsgründen weiterhin customization, und der Host projiziert diese Werte in das aktuelle Kind-Ergebnis theming.global, bevor die Seite die wippy-context-2.0-Konfiguration erhält.

Overrides setzen

Registry-Seiten (empfohlen): Setzen Sie meta.config_overrides in der _index.yaml der Seite. Der Host nimmt es in die Antwort der Content-API auf und injiziert es automatisch.

Eigenständige Pakete: Setzen Sie wippy.configOverrides in der package.json der Seite.

Manuell / zum Testen: Setzen Sie window.__WIPPY_CONFIG_OVERRIDES__ in einem <script>-Tag, das vor proxy.js läuft.

window.__WIPPY_CONFIG_OVERRIDES__ = {
  customization: {
    cssVariables: {
      '--p-primary': '#ff6b00',
    },
    customCSS: '.my-page-header { border-radius: 12px; }',
  },
}

Merge-Regeln

Feld Merge-Verhalten
cssVariables Ersetzt die Werte des Hosts — die Seite liefert ihr eigenes Theme
customCSS Ersetzt den Wert des Hosts
iconSets Additiv gemergt
axiosDefaults Tief gemergt
routePrefix Ersetzt
apiRoutes Tief gemergt

Jedes verschachtelte Kind, das die Seite einbettet — <w-iframe>, <w-artifact> und html.inject-Inhalte —, wird aus der bereits gemergten Konfiguration der Seite gebaut und erbt sie automatisch, rekursiv über den Unterbaum. Die Overrides einer Seite (insbesondere Theming) übertragen sich also auf alles darunter, nicht nur auf die Seite selbst.


Vue-Hilfsfunktionen

installVueWarnSuppressor(app)

Verfügbar in der aktuellen stimmigen @wippy-fe/proxy-Familie. Unterdrückt [Vue warn]: Failed to resolve component: foo-bar für Tags, die über customElements.define(...) statt app.component(...) registriert wurden. Der Template-Compiler von Vue gibt diese Warnungen für Web-Component-Tags aus, die er nicht kennt — die Elemente rendern korrekt, aber die Konsole füllt sich mit Rauschen.

import { installVueWarnSuppressor } from '@wippy-fe/proxy'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
installVueWarnSuppressor(app)
app.use(router)
app.mount('#app')

Was unterdrückt wird:

  • Tags, die bereits über customElements.define(...) registriert sind — Systemtags (w-iframe, w-artifact, wippy-loading, wippy-error) und jedes Tag, das die Autoload-Pipeline registriert (loadByTagName, Scanner).
  • Tags, die der Namensform von Custom Elements entsprechen (^[a-z][a-z0-9]*-[a-z0-9-]*$) und noch nicht registriert sind — deckt das Zeitfenster ab, in dem Vue rendert, bevor das Autoload-Skript eintrifft.

Was weiterhin warnt:

  • Tippfehler bei PascalCase-Komponenten (<UsreCard />). Der Suppressor gleicht sie nicht gegen das Kebab-Muster ab, und customElements.get liefert undefined, sodass sie in die Konsole durchgereicht werden — das erhält das Signal, das echte Fehler vom Rauschen trennt.

Die Funktion ist idempotent: Ein zweiter Aufruf auf derselben app ist ein echtes No-Op. Ein Marker Symbol.for('@wippy-fe/proxy/vue-warn-suppressor-installed') wird auf app.config gesetzt; der Marker wird als VUE_WARN_SUPPRESSOR_INSTALLED_MARKER exportiert, für Test-Setups, die ihn über Neuladen hinweg zurücksetzen müssen.

War bereits ein warnHandler installiert, wird er als previous erhalten und für Warnungen aufgerufen, die der Suppressor nicht unterdrückt.

createAppRouter(routes, options?) aus @wippy-fe/router

Kanonische Factory für Memory-Router in srcdoc-Subapps. Ersetzt den Boilerplate, den derzeit jede Subapp dupliziert (Memory-History, afterEach-Routensynchronisation zum Host, @history-Abonnement):

import { createAppRouter } from '@wippy-fe/router'
import { config } from '@wippy-fe/proxy'
import { routes } from './routes'

const router = createAppRouter(routes, {
  initialPath: config.context?.route,
})
app.use(router)

Lade- und Fehlerkomponenten

Zwei Web Components werden über loading.js automatisch registriert (injiziert vor proxy.js). Es sind keine Imports und keine manuelle Registrierung nötig.

<wippy-loading>

Vollbild-Ladespinner mit themengerechten Farben.

Attribut Beschreibung
title Haupttext (z. B. "Loading...")
subtitle Sekundärtext
no-bg Boolean — transparenter Hintergrund für den Einsatz als Overlay
<wippy-loading title="Loading..." subtitle="Please wait"></wippy-loading>
<wippy-loading no-bg title="Loading page content..."></wippy-loading>

<wippy-error>

Vollbild-Fehleranzeige mit Einfärbung nach Schweregrad.

Attribut Werte Standard
title beliebiger String "Something went wrong"
message beliebiger String (leer)
icon circle, triangle, sad circle
severity danger, warning danger
no-bg Boolean (fehlt)
<wippy-error title="Failed to load" message="Server returned 500" severity="danger"></wippy-error>
<wippy-error title="Connection Lost" message="Retrying..." icon="triangle" severity="warning"></wippy-error>

Beide Komponenten verwenden Shadow DOM mit CSS-Variablen aus @wippy-fe/theme und enthalten fest hinterlegte Fallbacks für Kontexte ohne Theme.

Empfohlenes Muster für einfache HTML-Seiten:

<body>
  <wippy-loading id="loader" title="Loading..."></wippy-loading>
  <div id="content" style="display:none"><!-- Inhalt --></div>

  <script type="module">
    import { api, host } from '@wippy-fe/proxy'

    async function init() {
      try {
        // Daten holen, Seite aufbauen ...
        document.getElementById('loader').remove()
        document.getElementById('content').style.display = 'block'
      } catch (error) {
        const errorEl = document.createElement('wippy-error')
        errorEl.setAttribute('title', 'Initialization failed')
        errorEl.setAttribute('message', error.message)
        document.getElementById('loader').replaceWith(errorEl)
      }
    }
    init()
  </script>
</body>

Vue 3 — Einstieg app.html:

<div id="app">
  <wippy-loading title="Loading..."></wippy-loading>
</div>
<script type="module" src="./src/app.ts"></script>

Wenn Vue in #app mountet, ersetzt es das <wippy-loading>-Element automatisch.