From dee29fa6b34fb0480a7c62b6a0eebe20a76d2521 Mon Sep 17 00:00:00 2001 From: "K. O. A." Date: Sun, 5 Jul 2026 22:02:35 -0400 Subject: [PATCH 1/2] best-practices: add "Integrating with the app" section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a section on reacting to and driving the app via window.feedBack, the biggest gap found in a completeness sweep of the real plugin surface. Rules (30-33): - Subscribe to app state through the event bus: catalog the commonly-used events (screen:changed, song:loading/ready/play/pause/stop/ended/seek/position-changed/ arrangement-changed, library:changed, viz:renderer:ready/reverted, highway:canvas-replaced/visibility) with their event.detail payloads; keep handlers cheap and unsubscribe when hidden. - Drive the app through the feedBack API (navigate/getNavParams/showScreen/ setReturnScreen, playSong/seek/setLoop/clearLoop/getLoop/playQueue, currentSong/isPlaying) — never the app's private DOM controls (#btn-loop-*). - Wrap Host functions carefully: always call and await the original, install once (idempotent), clean up on transitions, and don't assume load order. - Support both player UIs (v2/v3): detect uiVersion and mount into the Host- provided slot, verify in both. Renumber Shipping to 34-38 and add a checklist block. Docs only. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: K. O. A. --- CHANGELOG.md | 7 ++++ spec/best-practices.md | 89 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1603f86..9ce6268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,13 @@ for how the document, manifest, and per-plugin versions relate. servable) referenced by **absolute `/api/plugins//…` URLs** (relative resolves against the document, not the script); and **idempotent** runtime loading so re-hydration doesn't double-load. Added a matching checklist block. Docs only. +- Best-practices guide: added an **"Integrating with the app"** section, ground-truthed against the + `window.feedBack` runtime surface. Adds the **event-bus catalog** (`screen:changed`, `song:*`, + `library:changed`, `viz:*`, `highway:*` with their `event.detail` payloads), the rule to **drive + the app through the `feedBack` API** (`navigate`/`getNavParams`/`playSong`/`seek`/`setLoop`/ + `currentSong`/`playQueue`) **rather than its DOM controls**, **wrapper discipline** for hooking + Host globals (call and `await` the original, install once, clean up, no load-order assumptions), + and the **v2/v3 player-chrome** mount contract. Added a matching checklist block. Docs only. ## [0.1.0] - 2026-07-05 diff --git a/spec/best-practices.md b/spec/best-practices.md index 51406c5..63df53e 100644 --- a/spec/best-practices.md +++ b/spec/best-practices.md @@ -471,9 +471,78 @@ function loadScriptOnce(src) { --- +## Integrating with the app + +Beyond mounting a screen, most plugins need to *react to* and *drive* the app — react to the song +that's playing, navigate, control the transport. The Host exposes this through the `window.feedBack` +object; reach for it rather than the app's own DOM. (As with the rest of the client runtime surface, +these names are the current Host contract, versioned by the Host — feature-detect before you rely on +one.) + +### 30. Subscribe to app state through the event bus + +`window.feedBack` is an event bus: `on(event, fn)`, `off(event, fn)`, and `emit(event, detail)`. The +Host emits lifecycle events over it and you subscribe to react; the payload rides on `event.detail`. +The commonly-used events: + +| Event | `event.detail` | Fires when | +|---|---|---| +| `screen:changed` | `{ id }` | The active screen changes (your screen becoming visible/hidden). | +| `song:loading` / `song:ready` | song info | A song starts loading / is ready to play. | +| `song:play` / `song:resume` / `song:pause` / `song:stop` / `song:ended` | — | Transport state changes. | +| `song:seek` / `song:position-changed` | `{ time, duration }` | The playhead moves. | +| `song:arrangement-changed` | arrangement | The user switches arrangement. | +| `library:changed` | `{ reason }` | The song library is rescanned/updated. | +| `viz:renderer:ready` / `viz:reverted` | `{ reason }` on revert | A visualization renderer starts / auto-reverts. | +| `highway:canvas-replaced` / `highway:visibility` | `{ … }` | The highway canvas is swapped / shown or hidden. | + +Treat any event you don't recognise as optional (the set grows over time), keep handlers cheap (some +fire during playback — see rule 9), and **unsubscribe when your screen is hidden or torn down** +(rule 13) so a background plugin isn't doing work on every transport tick. + +### 31. Drive the app through the `feedBack` API, not its DOM + +To navigate, play, or control playback, call the Host API — never click or mutate the app's own +controls (`document.querySelector('#btn-loop-…')` and friends are private and move between UI +versions). The current surface includes: + +- **Navigation:** `feedBack.navigate(screenId, params)` (and `getNavParams()` to read them on the + other side), `showScreen(id)`, `setReturnScreen(id)`. +- **Playback / transport:** `playSong(...)`, `seek(seconds, reason)`, `setLoop(a, b)` / + `clearLoop()` / `getLoop()`, the `playQueue` API, and the live read-only state `currentSong` / + `isPlaying`. + +Going through the API keeps you working when the app's markup changes and avoids fighting the Host +for control of the transport. + +### 32. Wrap Host functions carefully — call through, stay idempotent, clean up + +A common pattern is wrapping a Host global like `playSong` or `showScreen` to run your own logic +around it. Do it defensively: + +- **Always call — and `await` — the original.** Capture it, invoke it, return its result. Swallowing + it breaks playback/navigation for the whole app and every other plugin in the wrapper chain. +- **Install the wrapper once.** Store it behind a stable singleton (rule 12) so re-hydration doesn't + stack wrapper-on-wrapper. +- **Undo what a transition invalidates.** If you wrap `showScreen`, tear down your player-screen hooks + when the user navigates away. +- **Don't assume load order.** Plugins load alphabetically, so a Host global or another plugin's API + may not exist yet when your `script` runs — check for it at the moment you *use* it (or on the + relevant event), not at load time. + +### 33. Support both player UIs + +feedBack has two player chromes (`v2` and `v3`). A plugin that injects controls into the player MUST +work in both: detect the active one (the Host exposes a `uiVersion` and a `v3` mount point such as +`ui.playerControlSlot()`), mount into the Host-provided slot rather than a hard-coded container, and +verify your plugin in **both** UIs before shipping. Don't rely on any backward-compatibility shim — +it's a migration aid, not a contract. + +--- + ## Shipping & good citizenship -### 30. Fail soft, log clearly +### 34. Fail soft, log clearly - Use `context["log"]` (server) so your messages land in the Host log under your plugin's namespace. @@ -482,25 +551,25 @@ function loadScriptOnce(src) { - If a surface can't initialise, degrade to a reduced-but-working state rather than taking the whole plugin down. -### 31. Degrade gracefully across Host versions +### 35. Degrade gracefully across Host versions A plugin may run on a Host older than the one you developed against. Don't assume a `context` key or a client runtime API exists without a documented Host version guaranteeing it. If an optional surface isn't supported, your plugin's other surfaces must still work. -### 32. Only declare capabilities you actually implement +### 36. Only declare capabilities you actually implement `capabilities` and `standards` wire you into cross-plugin pipelines (diagnostics, capability inspection). Declaring a capability you don't service registers a phantom participant and breaks the pipeline. If you don't participate, omit both keys entirely. -### 33. Mind the security boundary +### 37. Mind the security boundary Your `routes` run arbitrary Python in the server process and your `script` runs in the app's renderer. Validate every route input, don't shell out on user data, and don't reach outside your plugin directory. Users installing your plugin are trusting it like an app extension — earn it. -### 34. Ship a README and a changelog +### 38. Ship a README and a changelog A plugin folder should carry a short `README.md` (what it does, which Host version it targets) and note changes per version. It costs little and saves every future reader — including you. @@ -565,6 +634,16 @@ note changes per version. It costs little and saves every future reader — incl `import`/`export` or top-level `await` in `screen.js`). - [ ] Runtime loads are de-duped so re-hydration doesn't load them twice. +**Integrating with the app:** + +- [ ] React to app state via the `window.feedBack` event bus; handlers are cheap and unsubscribe + when hidden. +- [ ] Navigation/playback goes through the `feedBack` API (`navigate`, `playSong`, `setLoop`, …), + never the app's own DOM controls. +- [ ] Any wrapped Host function calls + `await`s the original, installs once, and cleans up; no + load-order assumptions. +- [ ] Player-injecting plugins work in both `v2` and `v3` (mount into the Host slot). + **Capabilities & shipping:** - [ ] Capabilities/standards declared only if actually implemented; cross-plugin calls go through From f74c4c119fd76256f175129c3a17914d291de762 Mon Sep 17 00:00:00 2001 From: "K. O. A." Date: Mon, 6 Jul 2026 00:30:10 -0400 Subject: [PATCH 2/2] =?UTF-8?q?best-practices:=20rule=2031=20=E2=80=94=20s?= =?UTF-8?q?plit=20feedBack.*=20methods=20from=20legacy=20window=20globals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disambiguate the transport/nav surface: methods on window.feedBack (navigate/getNavParams/seek/setLoop/currentSong) vs legacy top-level globals (showScreen/playSong/setReturnScreen), and say to feature-detect and prefer the feedBack-namespaced call. Signed-off-by: K. O. A. --- spec/best-practices.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/spec/best-practices.md b/spec/best-practices.md index 63df53e..75a9595 100644 --- a/spec/best-practices.md +++ b/spec/best-practices.md @@ -504,13 +504,17 @@ fire during playback — see rule 9), and **unsubscribe when your screen is hidd To navigate, play, or control playback, call the Host API — never click or mutate the app's own controls (`document.querySelector('#btn-loop-…')` and friends are private and move between UI -versions). The current surface includes: - -- **Navigation:** `feedBack.navigate(screenId, params)` (and `getNavParams()` to read them on the - other side), `showScreen(id)`, `setReturnScreen(id)`. -- **Playback / transport:** `playSong(...)`, `seek(seconds, reason)`, `setLoop(a, b)` / - `clearLoop()` / `getLoop()`, the `playQueue` API, and the live read-only state `currentSong` / - `isPlaying`. +versions). The surface comes in two forms; **feature-detect** whichever one you call before relying +on it: + +- **On the `window.feedBack` object:** `feedBack.navigate(screenId, params)` and + `feedBack.getNavParams()`, `feedBack.seek(seconds, reason)`, `feedBack.setLoop(a, b)` / + `feedBack.clearLoop()` / `feedBack.getLoop()`, and the live read-only state `feedBack.currentSong` + / `feedBack.isPlaying`. +- **Legacy top-level globals** (supported but being migrated behind `feedBack`, per rule 32's + caution about the surface): `window.showScreen(id)`, `window.playSong(...)`, + `window.setReturnScreen(id)`, and the `window.feedBack.playQueue` queue API. Prefer the + `feedBack`-namespaced call where one exists. Going through the API keeps you working when the app's markup changes and avoids fighting the Host for control of the transport.