[Plugin registry]

PluginRegistry singleton: ordered maps per kind, listeners, built-in protection, and bootstrap wiring.

Plugin registry

Abstract

PluginRegistry (frontend/src/plugins/registry.ts) is the single in-memory source of truth for every installed plugin of every kind. Catalogs, the dynamic loader, active resolution, and Manager UI all read/write through this class (or thin wrappers that call it).

Conceptual model

Interface surface

Singleton

import { registry, PluginRegistry } from './plugins/registry';
// or from './plugins' barrel

Per-kind API (pattern)

MethodBehavior
registerSource(p)Assert contract; set map; append order if new; emit registered
getSource(id)Map lookup
listSources()Registration order, not insertion-hash order
unregisterSource(id, { allowBuiltIn? })Refuses builtIn unless opted in
Same pattern for stream / engine / storage
registerComponent / listComponentsPhase-2 components (unordered Map values)

Bulk

MethodBehavior
register(plugin: AnyPlugin)Dispatch on kind
unregister(kind, id, opts?)Kind switch
clear()Wipe all maps and order arrays (tests)
summary(){ sources, streams, engines, storages } lightweight summaries
on(listener)Subscribe to { type, kind, id }; returns unsubscribe

Order preservation

Private arrays _sourceOrder, _streamOrder, _engineOrder, _storageOrder ensure UI dropdowns stay stable when plugins re-register (idempotent set without reordering).

Built-in protection

if (p.builtIn && !opts?.allowBuiltIn) return false;

Dynamic uninstall paths must never silently delete binance-rest / server / local.

Internals (repo paths)

ConcernPath
Class + singletonfrontend/src/plugins/registry.ts
Idempotent builtinsfrontend/src/plugins/bootstrap.tsensureBuiltins()
Active setfrontend/src/plugins/active.ts
Dynamic register helperssources/catalog.ts registerDynamicSource, etc.

Bootstrap

// bootstrap.ts — once per page lifetime
ensureSourcesRegistered();
ensureStreamsRegistered();
ensureEnginesRegistered();
ensureStoragesRegistered();

Safe to call repeatedly: catalogs use a local registered flag; bootstrap uses done.

Active resolution defaults

SlotDefault id
sourcebinance-rest
streambinance-ws
engineserver
storagelocal

Fallbacks chain: store.activePlugins.* → legacy store fields → default → registry get with hard fallback id → throw if still missing (source/stream/engine). Storage returns undefined if nothing registered (should not happen after builtins).

Engine endpoint surface

getActiveEngineConfig() injects store.endpoint when the engine has an endpoint config field or id is server — keeps the topbar Endpoint field and engine config aligned.

Invariants & edge cases

  1. Re-register same id — overwrites the plugin object; does not duplicate order entry.
  2. Listener errors are swallowed — one bad subscriber cannot break registration.
  3. clear() does not reset bootstrap/catalog flags — tests use _resetBootstrapFlag / _reset*RegistrationFlag helpers.
  4. Components — no order array; listComponents is Map iteration order.

Worked examples

Register a test source

import { registry } from '../plugins/registry';

registry.registerSource({
  id: 'fixture',
  name: 'Fixture',
  kind: 'source',
  async fetchHistorical() {
    return [{ time: 1, open: 1, high: 1, low: 1, close: 1 }];
  },
});

Listen for dynamic installs

const off = registry.on((ev) => {
  if (ev.type === 'registered' && ev.kind === 'engine') {
    console.log('engine ready', ev.id);
  }
});
// later: off()

Failure modes

SymptomCause
source: kind must be 'source'Wrong discriminant on register
Dropdown empty after clear() in testForgot re-ensureBuiltins / reset flags
Unregister returns falseBuilt-in protection

See also