Fluxta

Editor API

Everything an editor page can do.

Editors are web pages that Fluxta mounts in a sandboxed iframe. There are two, and they share most of their surface:

  • ActionEditor — the settings form for one placed Action.
  • PluginEditor — the settings page for the plugin as a whole.
import { ActionEditor, PluginEditor } from "@fluxta/sdk/api";

Action Editor

const editor = new ActionEditor();
await editor.connect();

ActionEditor

new ActionEditor()

Throws unless it is running inside the Fluxta editor iframe. Specifically, it throws when:

  • there is no browser window,
  • the URL fragment carries no editor grant, or
  • the query string carries no id for the Action being edited.

Fluxta supplies both; neither is constructed by the plugin. The practical consequence is that an editor cannot run standalone against a dev server — build it and let Fluxta mount it.

connect

connect(): Promise<void>

Opens the connection and resolves once it is established. Reconnects after ordinary network failures; rejects on authorization, policy, and version failures, which are final.

getActionSettings

getActionSettings(): Promise<JsonValue>

The settings currently saved for this placement. Use it to fill in the form when the editor mounts.

const settings = (await editor.getActionSettings()) as Settings;

Returns {} when nothing has been saved yet, when the stored value is null, and when it cannot be parsed. A parse error never needs handling, but the result should be treated as untrusted and its shape validated.

onActionSave

onActionSave(handler: () => unknown): () => void

Registers the function Fluxta calls when the user saves. Whatever it returns is serialized to JSON and stored as this placement's settings. Returns a function that unregisters the handler.

const off = editor.onActionSave(() => ({ scene: selectedScene }));

Only one handler is live at a time

Registering a second handler replaces the first. With React state, re-register on every change — or read from a ref — or the handler keeps saving the value it closed over when it was first registered.

useEffect(() => editor.onActionSave(() => ({ scene })), [scene]);

If the handler throws, or returns something that cannot be serialized — that includes returning nothing at all — the error is logged and the save stores nothing rather than a half-serialized value. The previously stored settings are kept, and Fluxta reports the failed save in the editor dialog instead of closing it.

Run Variables

The Run Variables readable at this Action's position — what an author could reference from this Action's settings right now. Fluxta works the set out so a form can offer a picker instead of asking people to remember an address.

getRunVariables

getRunVariables(): RunVariableDescriptor[]

The current list. Empty until Fluxta's first push arrives, shortly after the editor mounts. The same array comes back until the next push, so it works as a useSyncExternalStore snapshot.

onRunVariablesChange

onRunVariablesChange(listener): () => void

Subscribes to updates. Fluxta pushes a new list whenever the surrounding context changes — steps reordered, an Event's subscriptions edited — so an open editor stays current. Returns an unsubscribe function.

The listener is not called with the current list, only with pushes that arrive after it subscribed. Read getRunVariables() once subscribed, not only before — Fluxta sends the first list just once, and a push between the two is otherwise missed. In React, useSyncExternalStore does both in the right order:

const subscribe = (onChange: () => void) =>
  editor.onRunVariablesChange(onChange);
const getSnapshot = () => editor.getRunVariables();

export function useRunVariables() {
  return useSyncExternalStore(subscribe, getSnapshot);
}

RunVariableDescriptor

type RunVariableDescriptor = {
  address: string; // "<plugin>.<action-or-event>.<key>"
  label: string; // human-readable name
  type: "string" | "number" | "boolean";
};

referenceTo

referenceTo(variable): string

Turns a descriptor into the text to insert into a settings field — the address wrapped in {{ }}.

import { referenceTo } from "@fluxta/sdk/api";

referenceTo(variable); // "{{obs.switch-scene.previousScene}}"

Nothing sensitive travels in the list

The list carries an address, a label, and a type. No credentials, and nothing from another plugin's settings.

TemplateField is a ready-made input that does all of this, for editors that do not need a custom picker.

Plugin Editor

const editor = new PluginEditor();
await editor.connect();

PluginEditor is BaseEditor with nothing added. It has no Action settings, no Run Variables, and no id — it is not editing any particular placement.

PluginEditor

new PluginEditor()

Throws unless it is running inside the Fluxta settings iframe: it needs a browser window and an editor grant in the URL fragment.

Which plugin it belongs to comes from that grant, not from anything passed in, so an editor cannot reach another plugin's settings.

Mount it by declaring a top-level editor in the manifest, and use editorPlacement to choose between the settings page and its own navigation item.

Shared surface

Everything below is on both editors.

getPluginSettings

getPluginSettings(): Promise<{ event: string; payload: JsonValue }>

Fetches the plugin-wide settings — the same store the background process reads. The value is on payload.

const settings = await editor.getPluginSettings();
const config = settings.payload;

These are answered by Fluxta itself, so they work even when the background process is not running.

setPluginSettings

setPluginSettings(data: JsonValue): Promise<void>

Replaces the stored plugin settings wholesale.

await editor.setPluginSettings({ host, port });

sendToPlugin

sendToPlugin<T extends JsonValue>(data: T): Promise<void>

Sends a message to the background process, received there via onReceiveFromEditor.

await editor.sendToPlugin({ event: "testConnection" });

If the background process is not running, the message goes nowhere — no error and no queue. Design the editor to work with the plugin offline, and to ask again once it hears from it.

onReceiveFromPlugin

onReceiveFromPlugin<T>(listener): () => void

Receives what the background process sent with sendToEditor. Returns an unsubscribe function.

const off = editor.onReceiveFromPlugin<Status>((data) => setStatus(data));

onTerminal

onTerminal(listener: (error: Error) => void): () => void

Observes a final disconnect after a successful connect. Nothing is retried afterwards; show the user that the editor needs reopening.

A typical Action Editor

import { ActionEditor } from "@fluxta/sdk/api";
import { useEffect, useState } from "react";

const editor = new ActionEditor();
await editor.connect();

export function App() {
  const [scene, setScene] = useState("");
  const [scenes, setScenes] = useState<string[]>([]);

  // 1. Fill the form from what was saved.
  useEffect(() => {
    editor.getActionSettings().then((saved) => {
      setScene((saved as { scene?: string }).scene ?? "");
    });
  }, []);

  // 2. Tell Fluxta what to store. Re-registered whenever `scene` changes.
  useEffect(() => editor.onActionSave(() => ({ scene })), [scene]);

  // 3. Take live data from the background process...
  useEffect(
    () =>
      editor.onReceiveFromPlugin<{ event: string; scenes: string[] }>(
        (data) => {
          if (data.event === "scenes") setScenes(data.scenes);
        },
      ),
    [],
  );

  // 4. ...and ask for it on mount.
  useEffect(() => {
    void editor.sendToPlugin({ event: "getScenes" });
  }, []);

  return <SceneSelect value={scene} options={scenes} onChange={setScene} />;
}

On this page