Skip to main content
IPC (inter-process communication) handlers let your plugin’s iframe UI trigger logic in the plugin backend. You register named handler functions in init(), and the UI calls them by name using window.createBackendReq.

Registering a handler

Call PluginRegistry.registerHandler(name, handler) inside init(). The name is a string the UI uses to identify which handler to invoke.
import { PluginRegistry } from "@anikitenko/fdo-sdk";

init(): void {
  PluginRegistry.registerHandler("greet", (data: unknown) => {
    const input = data as { name: string };
    return { message: `Hello, ${input.name}!` };
  });
}
Handler signature:
type PluginHandler<TInput = unknown, TOutput = unknown> =
  (data: TInput) => TOutput | Promise<TOutput>;
Handlers can be synchronous or async. The SDK awaits the result either way.
Always register handlers in init(), not in render(). Handlers registered in render() get re-registered on every render cycle, which causes stale duplicate entries in the handler registry.

Calling handlers from the UI

The FDO host injects window.createBackendReq into the iframe. Pass the handler name and a data payload:
// Inside your render() output or inline <script>:
const result = await window.createBackendReq("greet", { name: "Alice" });
console.log(result.message); // "Hello, Alice!"
window.createBackendReq returns a Promise that resolves to whatever the handler returned. If the handler returns null or throws, the promise still resolves (to null) — the SDK catches handler exceptions and logs them.

Structuring request and response data

Use plain objects that are JSON-serializable. Avoid passing class instances, functions, or circular references:
// Good: plain objects
PluginRegistry.registerHandler("saveSettings", (data: unknown) => {
  const { theme, fontSize } = data as { theme: string; fontSize: number };
  // ... save to store ...
  return { success: true, saved: { theme, fontSize } };
});

// In the UI:
const result = await window.createBackendReq("saveSettings", {
  theme: "dark",
  fontSize: 14,
});
if (result.success) {
  console.log("Settings saved:", result.saved);
}

Async handlers

Handlers can be async and return a Promise. The SDK awaits them transparently:
PluginRegistry.registerHandler("fetchData", async (data: unknown) => {
  // Simulate an async operation
  await new Promise(resolve => setTimeout(resolve, 200));
  return { items: ["a", "b", "c"] };
});
For handlers that may take a noticeable amount of time, show a loading state in the UI immediately after calling window.createBackendReq and clear it once the promise resolves.

Error handling

The SDK catches exceptions thrown by handlers and returns null to the UI rather than propagating the error. Log errors inside your handler so you can debug them:
PluginRegistry.registerHandler("riskyOperation", async (data: unknown) => {
  try {
    // ... operation ...
    return { ok: true };
  } catch (err) {
    this.error(err as Error);
    return { ok: false, error: "Operation failed" };
  }
});
On the UI side, always check for a null response or an error field:
const result = await window.createBackendReq("riskyOperation", {});
if (!result || !result.ok) {
  console.error("Handler failed:", result?.error);
}

Complete example

The following is based on examples/02-interactive-plugin.ts and shows handler registration, async handlers, and UI calls:
import {
  FDO_SDK,
  FDOInterface,
  PluginMetadata,
  PluginRegistry,
  DOMText,
  DOMNested,
  DOMButton,
} from "@anikitenko/fdo-sdk";

export default class InteractivePlugin extends FDO_SDK implements FDOInterface {
  private readonly _metadata: PluginMetadata = {
    name: "Interactive Plugin Example",
    version: "1.0.0",
    author: "FDO SDK Team",
    description: "Demonstrates interactive UI with buttons, forms, and message handlers",
    icon: "widget-button"
  };

  private counter: number = 0;

  get metadata(): PluginMetadata {
    return this._metadata;
  }

  init(): void {
    try {
      this.log("InteractivePlugin initialized!");

      PluginRegistry.registerHandler("incrementCounter", (data: unknown) => {
        this.counter++;
        this.log(`Counter incremented to ${this.counter}`);
        return { success: true, counter: this.counter };
      });

      PluginRegistry.registerHandler("decrementCounter", (data: unknown) => {
        this.counter--;
        this.log(`Counter decremented to ${this.counter}`);
        return { success: true, counter: this.counter };
      });

      PluginRegistry.registerHandler("submitForm", async (data: any) => {
        this.log(`Form submitted with data: ${JSON.stringify(data)}`);
        await new Promise(resolve => setTimeout(resolve, 500));
        const userName = data.userName || "Guest";
        return {
          success: true,
          message: `Welcome, ${userName}! Your form has been processed.`,
          timestamp: new Date().toISOString()
        };
      });

    } catch (error) {
      this.error(error as Error);
    }
  }

  render(): string {
    const domText = new DOMText();
    const domNested = new DOMNested();
    const domButton = new DOMButton();

    return domNested.createBlockDiv([
      domText.createHText(1, this._metadata.name),

      domNested.createBlockDiv([
        domText.createHText(3, "Counter Example"),
        domText.createPText(`Current count: ${this.counter}`),

        domButton.createButton(
          "Increment",
          () => window.createBackendReq("incrementCounter", {}),
          { style: { padding: "10px 20px", marginRight: "10px", cursor: "pointer" } }
        ),

        domButton.createButton(
          "Decrement",
          () => window.createBackendReq("decrementCounter", {}),
          { style: { padding: "10px 20px", cursor: "pointer" } }
        ),
      ], { style: { marginTop: "20px", padding: "15px", backgroundColor: "#f0f0f0", borderRadius: "5px" } }),

      `<script>
        async function handleFormSubmit() {
          const userName = document.getElementById("userName").value;
          const resultDiv = document.getElementById("form-result");
          resultDiv.textContent = "Processing...";
          try {
            const result = await window.createBackendReq("submitForm", { userName });
            resultDiv.textContent = result?.success ? result.message : "Submission failed.";
          } catch (error) {
            resultDiv.textContent = "An error occurred.";
          }
        }
      </script>`,
    ], {
      style: { padding: "20px", fontFamily: "Arial, sans-serif" }
    });
  }
}

new InteractivePlugin();

Built-in diagnostics handler

The SDK registers a reserved handler named __sdk.getDiagnostics (accessible via PluginRegistry.DIAGNOSTICS_HANDLER). The FDO host can query it to inspect the plugin’s runtime health, registered handlers, capability grants, and notification history — without requiring any custom handler code in your plugin. Example host request payload:
{
  message: "UI_MESSAGE",
  content: {
    handler: "__sdk.getDiagnostics",
    content: { notificationsLimit: 20 }
  }
}
The response includes apiVersion, pluginId, health counters, registered handler names, store info, and recent notifications. You can use this during development to verify your handlers are registered correctly.