Appearance
kapp.plugin (pont overlay ↔ plugins)
Une appli peut avoir un plugin viewers (interface publique jouée par les spectateurs) et/ou un plugin streamer, en plus de l'overlay. kapp.plugin est le pont temps réel entre ces contextes.
Bien distinguer les deux plugins
- Le plugin viewers est public et potentiellement ouvert par beaucoup de personnes en même temps. Tout événement reçu est une intention non fiable : l'appli doit revalider l'état, les droits, le coût et les règles du jeu avant d'agir.
- Le plugin streamer est privé, mais son nom est historique : le streamer et les admins autorisés du channel peuvent l'ouvrir, avec plusieurs onglets. Ne suppose jamais qu'il n'existe qu'une seule instance ou un seul opérateur.
- Les deux flux sont limités séparément. Le plugin streamer/admin dispose d'une marge plus large ; un afflux viewers ne consomme pas sa réserve.
Déclaration (infos.json)
viewers-plugin/viewers-plugin-index/viewers-plugin-height— page viewers.streamer-plugin/streamer-plugin-index— page streamer et admins.
Charger le helper officiel
Une nouvelle app charge toujours le helper V2 depuis l'origine Kappapps avec son URL absolue :
html
<script src="https://kappapps.app/assets/app-plugin/helper-v2.js"></script>Les chemins racine /assets/app-plugin/helper.js et /assets/app-plugin/helper-v2.js sont dépréciés. Ils restent disponibles sur la Pull Zone Bunny uniquement comme fallback figé pour les versions historiques déjà publiées. Ne les utilise dans aucun nouveau plugin et ne publie jamais un futur helper sur Bunny : toute nouvelle version de helper reste exclusivement hébergée par Kappapps et se charge par son URL absolue.
Les fichiers relatifs au dossier du plugin (style.css, ./icons/x.svg, etc.) restent relatifs. Pour partager un asset avec l'overlay ou un autre plugin, déclare public-assets puis utilise la base signée du helper V2 :
js
KappappsHost.onReady(() => {
const publicAssets = KappappsHost.publicAssetsUrl;
if (!publicAssets) throw new Error('public-assets absent de infos.json');
const imageUrl = `${publicAssets}maps/world.webp`;
});publicAssetsUrl vaut null si aucun dossier n'est déclaré et se termine toujours par / sinon. Ne remonte pas hors du dossier plugin avec ../assets : les plugins publiés et les assets publics ont des périmètres CDN distincts. Ne construis jamais toi-même une URL /api/media/apps/{slug}/{version}/public_assets/... : ce bridge est déprécié et ne reste disponible que pour les versions historiques déjà publiées. KappappsHost.jwt reste également disponible avec le helper V2, quelle que soit l'origine réelle (Kappapps, localhost ou CDN) du document. Lis ces deux valeurs à partir du callback onReady, après le handshake avec la page hôte.
Développement en localhost
Le serveur serve du Kappapps DEV HUB couvre aussi les deux plugins d'un brouillon. Il suffit de conserver leurs dossiers dans le runtime publié et de les déclarer dans infos.json : les pages viewers et streamer de Kappapps chargent automatiquement leurs fichiers depuis localhost, tandis que le helper officiel et les API event / data continuent de passer par Kappapps.
- Aucun serveur, port ou réglage localhost supplémentaire n'est nécessaire pour les plugins.
- Utilise des chemins relatifs pour les fichiers propres au plugin, mais conserve l'URL Kappapps absolue ci-dessus pour le helper officiel. Elle fonctionne aussi lorsque le document du plugin est servi par le DEV HUB en localhost.
- En localhost,
KappappsHost.publicAssetsUrlpointe directement sur le dossierpublic-assetsdu runtime servi par le DEV HUB. - Le plugin viewers local n'est testable que depuis la machine du développeur : chez une autre personne,
localhostdésigne sa propre machine. - Une version soumise ignore toujours
localhost_pathet retombe sur les fichiers publiés.
Valider le vrai shell avec un agent
- Lance
serve, puis appelleprepare_previewavecsurface: viewers_pluginousurface: streamer_plugin. - Ouvre immédiatement la
preview_urldans le navigateur/Playwright de l'agent. La page porte un badgeSESSION DEVet exposedata-dev-preview-ready="true"une fois le helper du plugin prêt. - Inspecte la coque et l'iframe indiquée par
frame.selector/frame.origin: console, réseau, responsive et interactions doivent être vérifiés dans ce contexte. - Utilise
open_managed_previewseulement si ce navigateur ne peut pas atteindre l'iframe localhost.
Cette preview n'est ni le Dashboard viewers, ni le Dashboard streamer. C'est un shell minimal dédié qui reconstitue le même bridge postMessage, les mêmes API JWT et les mêmes canaux socket sur le channel DEV. N'ouvre jamais directement l'URL localhost du plugin : elle peut sembler correcte tout en masquant une erreur d'API, de bridge ou de résolution d'URL.
Le ticket présent dans la première URL est one-shot et valable deux minutes. Il est échangé contre un cookie HttpOnly, puis disparaît lors de la redirection vers une URL propre. La session complète reste courte et est révoquée quand la preview est déchargée ou la session locale arrêtée.
Service
await emitViewers(users: User[] | null, event: string, data = {}): Promise<void>— émet vers le plugin viewers.users = null→ tous ;users = [...]→ ciblés.await emitStreamer(event: string, data = {}): Promise<void>— émet vers toutes les instances ouvertes du plugin streamer/admin.await callStreamer(): Promise<void>— notifie le streamer et ses admins (sans données).getViewersPluginUrl(): string— URL publique du plugin viewers ({host}/viewers/{channel}/{app_slug}).
Événements reçus
EVENTS.PLUGIN_VIEWERS— message d'un viewer :{ viewer: User, event: string, data?: any }.EVENTS.PLUGIN_STREAMER— message d'une instance streamer/admin :{ event: string, data?: any }.
Pattern (overlay envoie aux viewers, écoute leurs réponses)
js
(async function () {
kapp.events.on(kapp.plugin.EVENTS?.PLUGIN_VIEWERS ?? 'plugin.viewers', ({ viewer, event, data }) => {
if (event !== 'answer') return;
// Le serveur de l'appli reste autoritaire : revalider la partie et le joueur.
console.log(viewer.login, 'a répondu', data);
});
await kapp.plugin.emitViewers(null, 'question', { text: 'Prêt ?' });
})();Absorber les vagues sans casser les interactions
- Émets une intention utilisateur, pas un événement par
mousemove, frame, tick, frappe ou changement visuel. Debounce les saisies et regroupe les mises à jour naturellement regroupables. - Pour un état continu (curseur, position, volume), préfère
latest wins: une nouvelle valeur remplace la précédente encore en attente. - Pour des actions comptables (achat, vote, récompense), ne fusionne pas aveuglément : donne un identifiant métier et déduplique côté appli.
- Utilise
emitViewers(null, ...)pour une donnée commune. Ne fabrique pas une requête ciblée par viewer quand un broadcast suffit. - Si une partie accepte moins de joueurs que de viewers, laisse le plugin chargé en mode spectateur/lecture seule et fais gérer l'inscription par l'appli. Ne bloque pas arbitrairement le chargement des viewers suivants.
- Garde les payloads petits. Les données privées d'un joueur doivent rester ciblées ; ne les diffuse jamais avec
users = null.
Quotas, file d'attente et erreurs
- Le helper plugin V2 borne la file locale et le nombre de requêtes simultanées. Sur un
429, il respecteRetry-Afteret retente une seule fois l'événement refusé ; il ne retente pas automatiquement un timeout ou une erreur5xx. - Toujours faire
await KappappsHost.emit(...)et gérer le rejet. La promesse peut échouer si la file locale est pleine, si le délai expire, si le relais est indisponible ou si le quota reste dépassé. - Les vieux plugins restent compatibles avec l'API, mais ne bénéficient pas de la file et du délai du helper V2 : évite les rafales dans leur propre code.
- Ne boucle jamais immédiatement sur un échec. Affiche si nécessaire un état « envoi en cours » puis rends l'interface à nouveau interactive.
Observabilité et confidentialité
Kappapps mesure les quantités dans les deux sens (direction, appli, channel, résultat, ciblage et nombre de destinataires). Le nom de l'événement, le payload, l'identité du viewer et celle de l'admin ne sont pas journalisés. Ne compte donc pas sur ces métriques pour reconstruire des données métier : ajoute dans l'appli ses propres compteurs fonctionnels si nécessaire, sans données sensibles.
Pièges
- Vérifie la forme exacte des events et de
EVENTSdansapp-sdk.d.ts. - Toutes les instances streamer/admin ouvertes peuvent recevoir le même événement : leur affichage doit être idempotent.
- Ne construis pas un chemin partagé avec
../depuis le plugin : utiliseKappappsHost.publicAssetsUrlet vérifie qu'il n'est pasnull. - Un viewer qui a interdit ton appli (
viewers-optout, cf.reference/launch-modes.md) n'a plus accès à son plugin et ne reçoit plus lesemitViewers: ne compte pas sur une réponse de sa part.