Back to Nuclear

Host pattern

.agents/skills/host-pattern/SKILL.md

latest4.7 KB
Original Source

Host pattern

Every feature area follows the same three-layer structure:

  1. Host type - the contract (SDK, no implementation)
  2. API class - what plugins actually call (SDK, wraps the host)
  3. Host implementation - bridges the API to player internals (player)

Plugins call methods on an API class (e.g. api.Queue.addToQueue()), which holds a reference to the host and delegates to it. Plugins never touch a host directly.

Files to create/modify

SDK (packages/plugin-sdk/)

ActionFileWhat
Createsrc/types/yourDomain.tsYourDomainHost interface + related types
Createsrc/api/yourDomain.tsYourDomainAPI class
Modifysrc/api/index.tsAdd yourDomainHost option + YourDomain field to NuclearAPI
Modifysrc/index.tsExport types and API class

Player (packages/player/)

ActionFileWhat
Createsrc/services/yourDomainHost.tsHost implementation + singleton export
Modifysrc/services/plugins/createPluginAPI.tsPass singleton to NuclearPluginAPI
Modifysrc/services/logger.tsAdd domain to LOG_SCOPES (needed for reportError)

If the domain needs shared model types: create packages/model/src/yourDomain.ts and re-export from packages/model/src/index.ts.

API class pattern

Every API class follows this structure.

typescript
// packages/plugin-sdk/src/api/yourDomain.ts
export class YourDomainAPI {
  #host?: YourDomainHost;

  constructor(host?: YourDomainHost) {
    this.#host = host;
  }

  #withHost<T>(fn: (host: YourDomainHost) => T): T {
    const host = this.#host;
    if (!host) {
      throw new Error('YourDomain host not available');
    }
    return fn(host);
  }

  yourMethod(arg: SomeType) {
    return this.#withHost((host) => host.yourMethod(arg));
  }
}

Reference: packages/plugin-sdk/src/api/queue.ts, packages/plugin-sdk/src/api/dashboard.ts

Connecting to NuclearAPI

typescript
// packages/plugin-sdk/src/api/index.ts
import type { YourDomainHost } from '../types/yourDomain';
import { YourDomainAPI } from './yourDomain';

// Add to class:
readonly YourDomain: YourDomainAPI;

// Add to constructor opts type:
yourDomainHost?: YourDomainHost;

// Add to constructor body:
this.YourDomain = new YourDomainAPI(opts?.yourDomainHost);
typescript
// packages/player/src/services/plugins/createPluginAPI.ts
import { yourDomainHost } from '../../services/yourDomainHost';
// Add to NuclearPluginAPI constructor call:
yourDomainHost,

Host implementation

Hosts bridge the SDK contract to whatever backs the domain. Most commonly a Zustand store:

typescript
// packages/player/src/services/yourDomainHost.ts
import type { YourDomainHost } from '@nuclearplayer/plugin-sdk';
import { useYourDomainStore } from '../stores/yourDomainStore';

export const createYourDomainHost = (): YourDomainHost => ({
  yourMethod: (arg) => useYourDomainStore.getState().doThing(arg),
  getState: () => useYourDomainStore.getState().value,
});

export const yourDomainHost = createYourDomainHost();

A host can also have access to the provider registry (metadata, streaming, dashboard), resolving which registered provider to call. Two patterns:

Single provider (user picks one in Sources):

typescript
const getProvider = (providerId?: string) =>
  providersHost.get<YourProvider>(
    providerId ?? providersHost.getActive('yourkind'), 'yourkind',
  );

export const createYourDomainHost = (): YourDomainHost => ({
  async fetch(query, providerId?) {
    const provider = getProvider(providerId);
    if (!provider) throw new Error('No provider available');
    return provider.fetch(query);
  },
});

Fan-out (aggregate across all providers - dashboard):

typescript
export const createYourDomainHost = (): YourDomainHost => ({
  async fetchAll() {
    const providers = providersHost.list('yourkind') as YourProvider[];
    const results = await Promise.allSettled(providers.map(async (provider) => {
      const items = await provider.fetch();
      return { providerId: provider.id, providerName: provider.name, items };
    }));
    return results.filter(isFulfilled).map((r) => r.value);
  },
});

Reference: packages/player/src/services/metadataHost.ts (single), packages/player/src/services/dashboardHost.ts (fan-out)

Error handling

  • Store-backed hosts: let errors propagate naturally.
  • Provider-backed, single provider (metadata, streaming): throw errors so the user sees failures.
  • Provider-backed, fan-out (dashboard): catch per-provider so one failure doesn't block others. Log via reportError, return partial results.
  • MissingCapabilityError: always a plugin bug. Single-provider domains throw it; fan-out domains log it and skip the offending provider.